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

AddPeopleDialog.js 18KB

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