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 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654
  1. // @flow
  2. import _ from 'lodash';
  3. import React from 'react';
  4. import {
  5. ActivityIndicator,
  6. FlatList,
  7. KeyboardAvoidingView,
  8. Platform,
  9. SafeAreaView,
  10. TextInput,
  11. TouchableOpacity,
  12. View
  13. } from 'react-native';
  14. import { ColorSchemeRegistry } from '../../../../base/color-scheme';
  15. import { AlertDialog, openDialog } from '../../../../base/dialog';
  16. import { translate } from '../../../../base/i18n';
  17. import {
  18. Icon,
  19. IconCancelSelection,
  20. IconCheck,
  21. IconClose,
  22. IconPhone,
  23. IconSearch,
  24. IconShare
  25. } from '../../../../base/icons';
  26. import {
  27. AvatarListItem,
  28. HeaderWithNavigation,
  29. SlidingView,
  30. type Item
  31. } from '../../../../base/react';
  32. import { connect } from '../../../../base/redux';
  33. import { beginShareRoom } from '../../../../share-room';
  34. import { setAddPeopleDialogVisible } from '../../../actions.native';
  35. import AbstractAddPeopleDialog, {
  36. type Props as AbstractProps,
  37. type State as AbstractState,
  38. _mapStateToProps as _abstractMapStateToProps
  39. } from '../AbstractAddPeopleDialog';
  40. import styles, {
  41. AVATAR_SIZE,
  42. DARK_GREY
  43. } from './styles';
  44. type Props = AbstractProps & {
  45. /**
  46. * The color schemed style of the Header.
  47. */
  48. _headerStyles: Object,
  49. /**
  50. * True if the invite dialog should be open, false otherwise.
  51. */
  52. _isVisible: boolean,
  53. /**
  54. * Function used to translate i18n labels.
  55. */
  56. t: Function
  57. };
  58. type State = AbstractState & {
  59. /**
  60. * Boolean to show if an extra padding needs to be added to the bottom bar.
  61. */
  62. bottomPadding: boolean,
  63. /**
  64. * State variable to keep track of the search field value.
  65. */
  66. fieldValue: string,
  67. /**
  68. * True if a search is in progress, false otherwise.
  69. */
  70. searchInprogress: boolean,
  71. /**
  72. * An array of items that are selectable on this dialog. This is usually
  73. * populated by an async search.
  74. */
  75. selectableItems: Array<Object>
  76. };
  77. /**
  78. * Implements a special dialog to invite people from a directory service.
  79. */
  80. class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
  81. /**
  82. * Default state object to reset the state to when needed.
  83. */
  84. defaultState = {
  85. addToCallError: false,
  86. addToCallInProgress: false,
  87. bottomPadding: false,
  88. fieldValue: '',
  89. inviteItems: [],
  90. searchInprogress: false,
  91. selectableItems: []
  92. };
  93. /**
  94. * Ref of the search field.
  95. */
  96. inputFieldRef: ?TextInput;
  97. /**
  98. * TimeoutID to delay the search for the time the user is probably typing.
  99. */
  100. searchTimeout: TimeoutID;
  101. /**
  102. * Contrustor of the component.
  103. *
  104. * @inheritdoc
  105. */
  106. constructor(props: Props) {
  107. super(props);
  108. this.state = this.defaultState;
  109. this._keyExtractor = this._keyExtractor.bind(this);
  110. this._renderInvitedItem = this._renderInvitedItem.bind(this);
  111. this._renderItem = this._renderItem.bind(this);
  112. this._renderSeparator = this._renderSeparator.bind(this);
  113. this._onClearField = this._onClearField.bind(this);
  114. this._onCloseAddPeopleDialog = this._onCloseAddPeopleDialog.bind(this);
  115. this._onInvite = this._onInvite.bind(this);
  116. this._onPressItem = this._onPressItem.bind(this);
  117. this._onShareMeeting = this._onShareMeeting.bind(this);
  118. this._onTypeQuery = this._onTypeQuery.bind(this);
  119. this._setFieldRef = this._setFieldRef.bind(this);
  120. }
  121. /**
  122. * Implements {@code Component#componentDidUpdate}.
  123. *
  124. * @inheritdoc
  125. */
  126. componentDidUpdate(prevProps) {
  127. if (prevProps._isVisible !== this.props._isVisible) {
  128. // Clear state
  129. this._clearState();
  130. }
  131. }
  132. /**
  133. * Implements {@code Component#render}.
  134. *
  135. * @inheritdoc
  136. */
  137. render() {
  138. const {
  139. _addPeopleEnabled,
  140. _dialOutEnabled,
  141. _headerStyles
  142. } = this.props;
  143. const { inviteItems, selectableItems } = this.state;
  144. let placeholderKey = 'searchPlaceholder';
  145. if (!_addPeopleEnabled) {
  146. placeholderKey = 'searchCallOnlyPlaceholder';
  147. } else if (!_dialOutEnabled) {
  148. placeholderKey = 'searchPeopleOnlyPlaceholder';
  149. }
  150. return (
  151. <SlidingView
  152. onHide = { this._onCloseAddPeopleDialog }
  153. position = 'bottom'
  154. show = { this.props._isVisible } >
  155. <HeaderWithNavigation
  156. forwardDisabled = { this._isAddDisabled() }
  157. forwardLabelKey = 'inviteDialog.send'
  158. headerLabelKey = 'inviteDialog.header'
  159. onPressBack = { this._onCloseAddPeopleDialog }
  160. onPressForward = { this._onInvite } />
  161. <KeyboardAvoidingView
  162. behavior = 'padding'
  163. style = { styles.avoidingView }>
  164. <SafeAreaView style = { styles.dialogWrapper }>
  165. <View
  166. style = { styles.searchFieldWrapper }>
  167. <View style = { styles.searchIconWrapper }>
  168. { this.state.searchInprogress
  169. ? <ActivityIndicator
  170. color = { DARK_GREY }
  171. size = 'small' />
  172. : <Icon
  173. src = { IconSearch }
  174. style = { styles.searchIcon } />}
  175. </View>
  176. <TextInput
  177. autoCorrect = { false }
  178. autoFocus = { false }
  179. onBlur = { this._onFocused(false) }
  180. onChangeText = { this._onTypeQuery }
  181. onFocus = { this._onFocused(true) }
  182. placeholder = {
  183. this.props.t(`inviteDialog.${placeholderKey}`)
  184. }
  185. ref = { this._setFieldRef }
  186. style = { styles.searchField }
  187. value = { this.state.fieldValue } />
  188. { this._renderClearButton() }
  189. </View>
  190. { Boolean(inviteItems.length) && <View style = { styles.invitedList }>
  191. <FlatList
  192. data = { inviteItems }
  193. horizontal = { true }
  194. keyExtractor = { this._keyExtractor }
  195. keyboardShouldPersistTaps = 'always'
  196. renderItem = { this._renderInvitedItem } />
  197. </View> }
  198. <View style = { styles.resultList }>
  199. <FlatList
  200. ItemSeparatorComponent = { this._renderSeparator }
  201. data = { selectableItems }
  202. extraData = { inviteItems }
  203. keyExtractor = { this._keyExtractor }
  204. keyboardShouldPersistTaps = 'always'
  205. renderItem = { this._renderItem } />
  206. </View>
  207. </SafeAreaView>
  208. <SafeAreaView
  209. style = { [
  210. styles.bottomBar,
  211. _headerStyles.headerOverlay,
  212. this.state.bottomPadding ? styles.extraBarPadding : null ] }>
  213. { this._renderShareMeetingButton() }
  214. </SafeAreaView>
  215. </KeyboardAvoidingView>
  216. </SlidingView>
  217. );
  218. }
  219. /**
  220. * Clears the dialog content.
  221. *
  222. * @returns {void}
  223. */
  224. _clearState() {
  225. this.setState(this.defaultState);
  226. }
  227. /**
  228. * Returns an object capable of being rendered by an {@code AvatarListItem}.
  229. *
  230. * @param {Object} flatListItem - An item of the data array of the {@code FlatList}.
  231. * @returns {?Object}
  232. */
  233. _getRenderableItem(flatListItem) {
  234. const { item } = flatListItem;
  235. switch (item.type) {
  236. case 'phone':
  237. return {
  238. avatar: IconPhone,
  239. key: item.number,
  240. title: item.number
  241. };
  242. case 'user':
  243. return {
  244. avatar: item.avatar,
  245. key: item.id || item.user_id,
  246. title: item.name
  247. };
  248. default:
  249. return null;
  250. }
  251. }
  252. _invite: Array<Object> => Promise<Array<Object>>
  253. _isAddDisabled: () => boolean;
  254. _keyExtractor: Object => string
  255. /**
  256. * Key extractor for the flatlist.
  257. *
  258. * @param {Object} item - The flatlist item that we need the key to be
  259. * generated for.
  260. * @returns {string}
  261. */
  262. _keyExtractor(item) {
  263. return item.type === 'user' ? item.id || item.user_id : item.number;
  264. }
  265. _onClearField: () => void
  266. /**
  267. * Callback to clear the text field.
  268. *
  269. * @returns {void}
  270. */
  271. _onClearField() {
  272. this.setState({
  273. fieldValue: ''
  274. });
  275. // Clear search results
  276. this._onTypeQuery('');
  277. }
  278. _onCloseAddPeopleDialog: () => boolean
  279. /**
  280. * Closes the dialog.
  281. *
  282. * @returns {boolean}
  283. */
  284. _onCloseAddPeopleDialog() {
  285. if (this.props._isVisible) {
  286. this.props.dispatch(setAddPeopleDialogVisible(false));
  287. return true;
  288. }
  289. return false;
  290. }
  291. _onFocused: boolean => Function;
  292. /**
  293. * Constructs a callback to be used to update the padding of the field if necessary.
  294. *
  295. * @param {boolean} focused - True of the field is focused.
  296. * @returns {Function}
  297. */
  298. _onFocused(focused) {
  299. return () => {
  300. Platform.OS === 'android' && this.setState({
  301. bottomPadding: focused
  302. });
  303. };
  304. }
  305. _onInvite: () => void
  306. /**
  307. * Invites the selected entries.
  308. *
  309. * @returns {void}
  310. */
  311. _onInvite() {
  312. this._invite(this.state.inviteItems)
  313. .then(invitesLeftToSend => {
  314. if (invitesLeftToSend.length) {
  315. this.setState({
  316. inviteItems: invitesLeftToSend
  317. });
  318. this._showFailedInviteAlert();
  319. } else {
  320. this._onCloseAddPeopleDialog();
  321. }
  322. });
  323. }
  324. _onPressItem: Item => Function
  325. /**
  326. * Function to preapre a callback for the onPress event of the touchable.
  327. *
  328. * @param {Item} item - The item on which onPress was invoked.
  329. * @returns {Function}
  330. */
  331. _onPressItem(item) {
  332. return () => {
  333. const { inviteItems } = this.state;
  334. const finderKey = item.type === 'phone' ? 'number' : 'user_id';
  335. if (inviteItems.find(
  336. _.matchesProperty(finderKey, item[finderKey]))) {
  337. // Item is already selected, need to unselect it.
  338. this.setState({
  339. inviteItems: inviteItems.filter(
  340. element => item[finderKey] !== element[finderKey])
  341. });
  342. } else {
  343. // Item is not selected yet, need to add to the list.
  344. const items: Array<Object> = inviteItems.concat(item);
  345. this.setState({
  346. inviteItems: _.sortBy(items, [ 'name', 'number' ])
  347. });
  348. }
  349. };
  350. }
  351. _onShareMeeting: () => void
  352. /**
  353. * Shows the system share sheet to share the meeting information.
  354. *
  355. * @returns {void}
  356. */
  357. _onShareMeeting() {
  358. if (this.state.inviteItems.length > 0) {
  359. // The use probably intended to invite people.
  360. this._onInvite();
  361. } else {
  362. this.props.dispatch(beginShareRoom());
  363. }
  364. }
  365. _onTypeQuery: string => void
  366. /**
  367. * Handles the typing event of the text field on the dialog and performs the
  368. * search.
  369. *
  370. * @param {string} query - The query that is typed in the field.
  371. * @returns {void}
  372. */
  373. _onTypeQuery(query) {
  374. this.setState({
  375. fieldValue: query
  376. });
  377. clearTimeout(this.searchTimeout);
  378. this.searchTimeout = setTimeout(() => {
  379. this.setState({
  380. searchInprogress: true
  381. }, () => {
  382. this._performSearch(query);
  383. });
  384. }, 500);
  385. }
  386. /**
  387. * Performs the actual search.
  388. *
  389. * @param {string} query - The query to search for.
  390. * @returns {void}
  391. */
  392. _performSearch(query) {
  393. this._query(query).then(results => {
  394. this.setState({
  395. selectableItems: _.sortBy(results, [ 'name', 'number' ])
  396. });
  397. })
  398. .finally(() => {
  399. this.setState({
  400. searchInprogress: false
  401. }, () => {
  402. this.inputFieldRef && this.inputFieldRef.focus();
  403. });
  404. });
  405. }
  406. _query: (string) => Promise<Array<Object>>;
  407. /**
  408. * Renders a button to clear the text field.
  409. *
  410. * @returns {React#Element<*>}
  411. */
  412. _renderClearButton() {
  413. if (!this.state.fieldValue.length) {
  414. return null;
  415. }
  416. return (
  417. <TouchableOpacity
  418. onPress = { this._onClearField }
  419. style = { styles.clearButton }>
  420. <View style = { styles.clearIconContainer }>
  421. <Icon
  422. src = { IconClose }
  423. style = { styles.clearIcon } />
  424. </View>
  425. </TouchableOpacity>
  426. );
  427. }
  428. _renderInvitedItem: Object => React$Element<any> | null
  429. /**
  430. * Renders a single item in the invited {@code FlatList}.
  431. *
  432. * @param {Object} flatListItem - An item of the data array of the
  433. * {@code FlatList}.
  434. * @param {number} index - The index of the currently rendered item.
  435. * @returns {?React$Element<any>}
  436. */
  437. _renderInvitedItem(flatListItem, index): React$Element<any> | null {
  438. const { item } = flatListItem;
  439. const renderableItem = this._getRenderableItem(flatListItem);
  440. return (
  441. <TouchableOpacity onPress = { this._onPressItem(item) } >
  442. <View
  443. pointerEvents = 'box-only'
  444. style = { styles.itemWrapper }>
  445. <AvatarListItem
  446. avatarOnly = { true }
  447. avatarSize = { AVATAR_SIZE }
  448. avatarStatus = { item.status }
  449. avatarStyle = { styles.avatar }
  450. avatarTextStyle = { styles.avatarText }
  451. item = { renderableItem }
  452. key = { index }
  453. linesStyle = { styles.itemLinesStyle }
  454. titleStyle = { styles.itemText } />
  455. <Icon
  456. src = { IconCancelSelection }
  457. style = { styles.unselectIcon } />
  458. </View>
  459. </TouchableOpacity>
  460. );
  461. }
  462. _renderItem: Object => React$Element<any> | null
  463. /**
  464. * Renders a single item in the search result {@code FlatList}.
  465. *
  466. * @param {Object} flatListItem - An item of the data array of the
  467. * {@code FlatList}.
  468. * @param {number} index - The index of the currently rendered item.
  469. * @returns {?React$Element<*>}
  470. */
  471. _renderItem(flatListItem, index): React$Element<any> | null {
  472. const { item } = flatListItem;
  473. const { inviteItems } = this.state;
  474. let selected = false;
  475. const renderableItem = this._getRenderableItem(flatListItem);
  476. if (!renderableItem) {
  477. return null;
  478. }
  479. switch (item.type) {
  480. case 'phone':
  481. selected = inviteItems.find(_.matchesProperty('number', item.number));
  482. break;
  483. case 'user':
  484. selected = item.id
  485. ? inviteItems.find(_.matchesProperty('id', item.id))
  486. : inviteItems.find(_.matchesProperty('user_id', item.user_id));
  487. break;
  488. default:
  489. return null;
  490. }
  491. return (
  492. <TouchableOpacity onPress = { this._onPressItem(item) } >
  493. <View
  494. pointerEvents = 'box-only'
  495. style = { styles.itemWrapper }>
  496. <AvatarListItem
  497. avatarSize = { AVATAR_SIZE }
  498. avatarStatus = { item.status }
  499. avatarStyle = { styles.avatar }
  500. avatarTextStyle = { styles.avatarText }
  501. item = { renderableItem }
  502. key = { index }
  503. linesStyle = { styles.itemLinesStyle }
  504. titleStyle = { styles.itemText } />
  505. { selected && <Icon
  506. src = { IconCheck }
  507. style = { styles.selectedIcon } /> }
  508. </View>
  509. </TouchableOpacity>
  510. );
  511. }
  512. _renderSeparator: () => React$Element<*> | null
  513. /**
  514. * Renders the item separator.
  515. *
  516. * @returns {?React$Element<*>}
  517. */
  518. _renderSeparator() {
  519. return (
  520. <View style = { styles.separator } />
  521. );
  522. }
  523. /**
  524. * Renders a button to share the meeting info.
  525. *
  526. * @returns {React#Element<*>}
  527. */
  528. _renderShareMeetingButton() {
  529. const { _headerStyles } = this.props;
  530. return (
  531. <TouchableOpacity
  532. onPress = { this._onShareMeeting }>
  533. <Icon
  534. src = { IconShare }
  535. style = { [ _headerStyles.headerButtonText, styles.shareIcon ] } />
  536. </TouchableOpacity>
  537. );
  538. }
  539. _setFieldRef: ?TextInput => void
  540. /**
  541. * Sets a reference to the input field for later use.
  542. *
  543. * @param {?TextInput} input - The reference to the input field.
  544. * @returns {void}
  545. */
  546. _setFieldRef(input) {
  547. this.inputFieldRef = input;
  548. }
  549. /**
  550. * Shows an alert telling the user that some invitees were failed to be
  551. * invited.
  552. *
  553. * NOTE: We're using an Alert here because we're on a modal and it makes
  554. * using our dialogs a tad more difficult.
  555. *
  556. * @returns {void}
  557. */
  558. _showFailedInviteAlert() {
  559. this.props.dispatch(openDialog(AlertDialog, {
  560. contentKey: {
  561. key: 'inviteDialog.alertText'
  562. }
  563. }));
  564. }
  565. }
  566. /**
  567. * Maps part of the Redux state to the props of this component.
  568. *
  569. * @param {Object} state - The Redux state.
  570. * @returns {{
  571. * _isVisible: boolean
  572. * }}
  573. */
  574. function _mapStateToProps(state: Object) {
  575. return {
  576. ..._abstractMapStateToProps(state),
  577. _headerStyles: ColorSchemeRegistry.get(state, 'Header'),
  578. _isVisible: state['features/invite'].inviteDialogVisible
  579. };
  580. }
  581. export default translate(connect(_mapStateToProps)(AddPeopleDialog));