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

AddPeopleDialog.js 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657
  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. clearButtonMode = 'always' // iOS only
  180. onBlur = { this._onFocused(false) }
  181. onChangeText = { this._onTypeQuery }
  182. onFocus = { this._onFocused(true) }
  183. placeholder = {
  184. this.props.t(`inviteDialog.${placeholderKey}`)
  185. }
  186. ref = { this._setFieldRef }
  187. style = { styles.searchField }
  188. value = { this.state.fieldValue } />
  189. { this._renderAndroidClearButton() }
  190. </View>
  191. { Boolean(inviteItems.length) && <View style = { styles.invitedList }>
  192. <FlatList
  193. data = { inviteItems }
  194. horizontal = { true }
  195. keyExtractor = { this._keyExtractor }
  196. keyboardShouldPersistTaps = 'always'
  197. renderItem = { this._renderInvitedItem } />
  198. </View> }
  199. <View style = { styles.resultList }>
  200. <FlatList
  201. ItemSeparatorComponent = { this._renderSeparator }
  202. data = { selectableItems }
  203. extraData = { inviteItems }
  204. keyExtractor = { this._keyExtractor }
  205. keyboardShouldPersistTaps = 'always'
  206. renderItem = { this._renderItem } />
  207. </View>
  208. </SafeAreaView>
  209. <SafeAreaView
  210. style = { [
  211. styles.bottomBar,
  212. _headerStyles.headerOverlay,
  213. this.state.bottomPadding ? styles.extraBarPadding : null ] }>
  214. { this._renderShareMeetingButton() }
  215. </SafeAreaView>
  216. </KeyboardAvoidingView>
  217. </SlidingView>
  218. );
  219. }
  220. /**
  221. * Clears the dialog content.
  222. *
  223. * @returns {void}
  224. */
  225. _clearState() {
  226. this.setState(this.defaultState);
  227. }
  228. /**
  229. * Returns an object capable of being rendered by an {@code AvatarListItem}.
  230. *
  231. * @param {Object} flatListItem - An item of the data array of the {@code FlatList}.
  232. * @returns {?Object}
  233. */
  234. _getRenderableItem(flatListItem) {
  235. const { item } = flatListItem;
  236. switch (item.type) {
  237. case 'phone':
  238. return {
  239. avatar: IconPhone,
  240. key: item.number,
  241. title: item.number
  242. };
  243. case 'user':
  244. return {
  245. avatar: item.avatar,
  246. key: item.id || item.user_id,
  247. title: item.name
  248. };
  249. default:
  250. return null;
  251. }
  252. }
  253. _invite: Array<Object> => Promise<Array<Object>>
  254. _isAddDisabled: () => boolean;
  255. _keyExtractor: Object => string
  256. /**
  257. * Key extractor for the flatlist.
  258. *
  259. * @param {Object} item - The flatlist item that we need the key to be
  260. * generated for.
  261. * @returns {string}
  262. */
  263. _keyExtractor(item) {
  264. return item.type === 'user' ? item.id || item.user_id : item.number;
  265. }
  266. _onClearField: () => void
  267. /**
  268. * Callback to clear the text field.
  269. *
  270. * @returns {void}
  271. */
  272. _onClearField() {
  273. this.setState({
  274. fieldValue: ''
  275. });
  276. // Clear search results
  277. this._onTypeQuery('');
  278. }
  279. _onCloseAddPeopleDialog: () => boolean
  280. /**
  281. * Closes the dialog.
  282. *
  283. * @returns {boolean}
  284. */
  285. _onCloseAddPeopleDialog() {
  286. if (this.props._isVisible) {
  287. this.props.dispatch(setAddPeopleDialogVisible(false));
  288. return true;
  289. }
  290. return false;
  291. }
  292. _onFocused: boolean => Function;
  293. /**
  294. * Constructs a callback to be used to update the padding of the field if necessary.
  295. *
  296. * @param {boolean} focused - True of the field is focused.
  297. * @returns {Function}
  298. */
  299. _onFocused(focused) {
  300. return () => {
  301. Platform.OS === 'android' && this.setState({
  302. bottomPadding: focused
  303. });
  304. };
  305. }
  306. _onInvite: () => void
  307. /**
  308. * Invites the selected entries.
  309. *
  310. * @returns {void}
  311. */
  312. _onInvite() {
  313. this._invite(this.state.inviteItems)
  314. .then(invitesLeftToSend => {
  315. if (invitesLeftToSend.length) {
  316. this.setState({
  317. inviteItems: invitesLeftToSend
  318. });
  319. this._showFailedInviteAlert();
  320. } else {
  321. this._onCloseAddPeopleDialog();
  322. }
  323. });
  324. }
  325. _onPressItem: Item => Function
  326. /**
  327. * Function to preapre a callback for the onPress event of the touchable.
  328. *
  329. * @param {Item} item - The item on which onPress was invoked.
  330. * @returns {Function}
  331. */
  332. _onPressItem(item) {
  333. return () => {
  334. const { inviteItems } = this.state;
  335. const finderKey = item.type === 'phone' ? 'number' : 'user_id';
  336. if (inviteItems.find(
  337. _.matchesProperty(finderKey, item[finderKey]))) {
  338. // Item is already selected, need to unselect it.
  339. this.setState({
  340. inviteItems: inviteItems.filter(
  341. element => item[finderKey] !== element[finderKey])
  342. });
  343. } else {
  344. // Item is not selected yet, need to add to the list.
  345. const items: Array<Object> = inviteItems.concat(item);
  346. this.setState({
  347. inviteItems: _.sortBy(items, [ 'name', 'number' ])
  348. });
  349. }
  350. };
  351. }
  352. _onShareMeeting: () => void
  353. /**
  354. * Shows the system share sheet to share the meeting information.
  355. *
  356. * @returns {void}
  357. */
  358. _onShareMeeting() {
  359. if (this.state.inviteItems.length > 0) {
  360. // The use probably intended to invite people.
  361. this._onInvite();
  362. } else {
  363. this.props.dispatch(beginShareRoom());
  364. }
  365. }
  366. _onTypeQuery: string => void
  367. /**
  368. * Handles the typing event of the text field on the dialog and performs the
  369. * search.
  370. *
  371. * @param {string} query - The query that is typed in the field.
  372. * @returns {void}
  373. */
  374. _onTypeQuery(query) {
  375. this.setState({
  376. fieldValue: query
  377. });
  378. clearTimeout(this.searchTimeout);
  379. this.searchTimeout = setTimeout(() => {
  380. this.setState({
  381. searchInprogress: true
  382. }, () => {
  383. this._performSearch(query);
  384. });
  385. }, 500);
  386. }
  387. /**
  388. * Performs the actual search.
  389. *
  390. * @param {string} query - The query to search for.
  391. * @returns {void}
  392. */
  393. _performSearch(query) {
  394. this._query(query).then(results => {
  395. this.setState({
  396. selectableItems: _.sortBy(results, [ 'name', 'number' ])
  397. });
  398. })
  399. .finally(() => {
  400. this.setState({
  401. searchInprogress: false
  402. }, () => {
  403. this.inputFieldRef && this.inputFieldRef.focus();
  404. });
  405. });
  406. }
  407. _query: (string) => Promise<Array<Object>>;
  408. /**
  409. * Renders a button to clear the text field on Android.
  410. *
  411. * NOTE: For the best platform experience we use the native solution on iOS.
  412. *
  413. * @returns {React#Element<*>}
  414. */
  415. _renderAndroidClearButton() {
  416. if (Platform.OS !== 'android' || !this.state.fieldValue.length) {
  417. return null;
  418. }
  419. return (
  420. <TouchableOpacity
  421. onPress = { this._onClearField }
  422. style = { styles.clearButton }>
  423. <View style = { styles.clearIconContainer }>
  424. <Icon
  425. src = { IconClose }
  426. style = { styles.clearIcon } />
  427. </View>
  428. </TouchableOpacity>
  429. );
  430. }
  431. _renderInvitedItem: Object => React$Element<any> | null
  432. /**
  433. * Renders a single item in the invited {@code FlatList}.
  434. *
  435. * @param {Object} flatListItem - An item of the data array of the
  436. * {@code FlatList}.
  437. * @param {number} index - The index of the currently rendered item.
  438. * @returns {?React$Element<any>}
  439. */
  440. _renderInvitedItem(flatListItem, index): React$Element<any> | null {
  441. const { item } = flatListItem;
  442. const renderableItem = this._getRenderableItem(flatListItem);
  443. return (
  444. <TouchableOpacity onPress = { this._onPressItem(item) } >
  445. <View
  446. pointerEvents = 'box-only'
  447. style = { styles.itemWrapper }>
  448. <AvatarListItem
  449. avatarOnly = { true }
  450. avatarSize = { AVATAR_SIZE }
  451. avatarStatus = { item.status }
  452. avatarStyle = { styles.avatar }
  453. avatarTextStyle = { styles.avatarText }
  454. item = { renderableItem }
  455. key = { index }
  456. linesStyle = { styles.itemLinesStyle }
  457. titleStyle = { styles.itemText } />
  458. <Icon
  459. src = { IconCancelSelection }
  460. style = { styles.unselectIcon } />
  461. </View>
  462. </TouchableOpacity>
  463. );
  464. }
  465. _renderItem: Object => React$Element<any> | null
  466. /**
  467. * Renders a single item in the search result {@code FlatList}.
  468. *
  469. * @param {Object} flatListItem - An item of the data array of the
  470. * {@code FlatList}.
  471. * @param {number} index - The index of the currently rendered item.
  472. * @returns {?React$Element<*>}
  473. */
  474. _renderItem(flatListItem, index): React$Element<any> | null {
  475. const { item } = flatListItem;
  476. const { inviteItems } = this.state;
  477. let selected = false;
  478. const renderableItem = this._getRenderableItem(flatListItem);
  479. if (!renderableItem) {
  480. return null;
  481. }
  482. switch (item.type) {
  483. case 'phone':
  484. selected = inviteItems.find(_.matchesProperty('number', item.number));
  485. break;
  486. case 'user':
  487. selected = item.id
  488. ? inviteItems.find(_.matchesProperty('id', item.id))
  489. : inviteItems.find(_.matchesProperty('user_id', item.user_id));
  490. break;
  491. default:
  492. return null;
  493. }
  494. return (
  495. <TouchableOpacity onPress = { this._onPressItem(item) } >
  496. <View
  497. pointerEvents = 'box-only'
  498. style = { styles.itemWrapper }>
  499. <AvatarListItem
  500. avatarSize = { AVATAR_SIZE }
  501. avatarStatus = { item.status }
  502. avatarStyle = { styles.avatar }
  503. avatarTextStyle = { styles.avatarText }
  504. item = { renderableItem }
  505. key = { index }
  506. linesStyle = { styles.itemLinesStyle }
  507. titleStyle = { styles.itemText } />
  508. { selected && <Icon
  509. src = { IconCheck }
  510. style = { styles.selectedIcon } /> }
  511. </View>
  512. </TouchableOpacity>
  513. );
  514. }
  515. _renderSeparator: () => React$Element<*> | null
  516. /**
  517. * Renders the item separator.
  518. *
  519. * @returns {?React$Element<*>}
  520. */
  521. _renderSeparator() {
  522. return (
  523. <View style = { styles.separator } />
  524. );
  525. }
  526. /**
  527. * Renders a button to share the meeting info.
  528. *
  529. * @returns {React#Element<*>}
  530. */
  531. _renderShareMeetingButton() {
  532. const { _headerStyles } = this.props;
  533. return (
  534. <TouchableOpacity
  535. onPress = { this._onShareMeeting }>
  536. <Icon
  537. src = { IconShare }
  538. style = { [ _headerStyles.headerButtonText, styles.shareIcon ] } />
  539. </TouchableOpacity>
  540. );
  541. }
  542. _setFieldRef: ?TextInput => void
  543. /**
  544. * Sets a reference to the input field for later use.
  545. *
  546. * @param {?TextInput} input - The reference to the input field.
  547. * @returns {void}
  548. */
  549. _setFieldRef(input) {
  550. this.inputFieldRef = input;
  551. }
  552. /**
  553. * Shows an alert telling the user that some invitees were failed to be
  554. * invited.
  555. *
  556. * NOTE: We're using an Alert here because we're on a modal and it makes
  557. * using our dialogs a tad more difficult.
  558. *
  559. * @returns {void}
  560. */
  561. _showFailedInviteAlert() {
  562. this.props.dispatch(openDialog(AlertDialog, {
  563. contentKey: {
  564. key: 'inviteDialog.alertText'
  565. }
  566. }));
  567. }
  568. }
  569. /**
  570. * Maps part of the Redux state to the props of this component.
  571. *
  572. * @param {Object} state - The Redux state.
  573. * @returns {{
  574. * _isVisible: boolean
  575. * }}
  576. */
  577. function _mapStateToProps(state: Object) {
  578. return {
  579. ..._abstractMapStateToProps(state),
  580. _headerStyles: ColorSchemeRegistry.get(state, 'Header'),
  581. _isVisible: state['features/invite'].inviteDialogVisible
  582. };
  583. }
  584. export default translate(connect(_mapStateToProps)(AddPeopleDialog));