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.

DialInNumbersForm.js 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. import { StatelessDropdownMenu } from '@atlaskit/dropdown-menu';
  2. import ExpandIcon from '@atlaskit/icon/glyph/expand';
  3. import React, { Component } from 'react';
  4. import { connect } from 'react-redux';
  5. import { translate } from '../../base/i18n';
  6. import { updateDialInNumbers } from '../actions';
  7. const logger = require('jitsi-meet-logger').getLogger(__filename);
  8. const EXPAND_ICON = <ExpandIcon label = 'expand' />;
  9. /**
  10. * React {@code Component} responsible for fetching and displaying telephone
  11. * numbers for dialing into the conference. Also supports copying a selected
  12. * dial-in number to the clipboard.
  13. *
  14. * @extends Component
  15. */
  16. class DialInNumbersForm extends Component {
  17. /**
  18. * {@code DialInNumbersForm}'s property types.
  19. *
  20. * @static
  21. */
  22. static propTypes = {
  23. /**
  24. * The redux state representing the dial-in numbers feature.
  25. */
  26. _dialIn: React.PropTypes.object,
  27. /**
  28. * The url for retrieving dial-in numbers.
  29. */
  30. dialInNumbersUrl: React.PropTypes.string,
  31. /**
  32. * Invoked to send an ajax request for dial-in numbers.
  33. */
  34. dispatch: React.PropTypes.func,
  35. /**
  36. * Invoked to obtain translated strings.
  37. */
  38. t: React.PropTypes.func
  39. }
  40. /**
  41. * Initializes a new {@code DialInNumbersForm} instance.
  42. *
  43. * @param {Object} props - The read-only properties with which the new
  44. * instance is to be initialized.
  45. */
  46. constructor(props) {
  47. super(props);
  48. this.state = {
  49. /**
  50. * Whether or not the dropdown should be open.
  51. *
  52. * @type {boolean}
  53. */
  54. isDropdownOpen: false,
  55. /**
  56. * The dial-in number to display as currently selected in the
  57. * dropdown. The value should be an object which has two key/value
  58. * pairs, content and number. The value of "content" will display in
  59. * the dropdown while the value of "number" is a substring of
  60. * "content" which will be copied to clipboard.
  61. *
  62. * @type {object}
  63. */
  64. selectedNumber: null
  65. };
  66. /**
  67. * The internal reference to the DOM/HTML element backing the React
  68. * {@code Component} input. It is necessary for the implementation of
  69. * copying to the clipboard.
  70. *
  71. * @private
  72. * @type {HTMLInputElement}
  73. */
  74. this._inputElement = null;
  75. // Bind event handlers so they are only bound once for every instance.
  76. this._onClick = this._onClick.bind(this);
  77. this._onOpenChange = this._onOpenChange.bind(this);
  78. this._onSelect = this._onSelect.bind(this);
  79. this._setInput = this._setInput.bind(this);
  80. }
  81. /**
  82. * Dispatches a request for numbers if not already present in the redux
  83. * store. If numbers are present, sets a default number to display in the
  84. * dropdown trigger.
  85. *
  86. * @inheritdoc
  87. * returns {void}
  88. */
  89. componentDidMount() {
  90. if (this.props._dialIn.numbers) {
  91. this._setDefaultNumber(this.props._dialIn.numbers);
  92. } else {
  93. this.props.dispatch(
  94. updateDialInNumbers(this.props.dialInNumbersUrl));
  95. }
  96. }
  97. /**
  98. * Monitors for number updates and sets a default number to display in the
  99. * dropdown trigger if not already set.
  100. *
  101. * @inheritdoc
  102. * returns {void}
  103. */
  104. componentWillReceiveProps(nextProps) {
  105. if (!this.state.selectedNumber && nextProps._dialIn.numbers) {
  106. this._setDefaultNumber(nextProps._dialIn.numbers);
  107. }
  108. }
  109. /**
  110. * Implements React's {@link Component#render()}.
  111. *
  112. * @inheritdoc
  113. * @returns {ReactElement}
  114. */
  115. render() {
  116. const { t, _dialIn } = this.props;
  117. const numbers = _dialIn.numbers;
  118. const items = numbers ? this._formatNumbers(numbers) : [];
  119. const isEnabled = this._isDropdownEnabled();
  120. const inputWrapperClassNames
  121. = `form-control__container ${isEnabled ? '' : 'is-disabled'}
  122. ${_dialIn.loading ? 'is-loading' : ''}`;
  123. let triggerText = '';
  124. if (!_dialIn.numbersEnabled) {
  125. triggerText = t('invite.numbersDisabled');
  126. } else if (this.state.selectedNumber
  127. && this.state.selectedNumber.content) {
  128. triggerText = this.state.selectedNumber.content;
  129. } else if (!numbers && _dialIn.loading) {
  130. triggerText = t('invite.loadingNumbers');
  131. } else if (_dialIn.error) {
  132. triggerText = t('invite.errorFetchingNumbers');
  133. } else {
  134. triggerText = t('invite.noNumbers');
  135. }
  136. return (
  137. <div className = 'form-control dial-in-numbers'>
  138. <label className = 'form-control__label'>
  139. { t('invite.dialInNumbers') }
  140. </label>
  141. <div className = { inputWrapperClassNames }>
  142. { this._createDropdownMenu(items, triggerText) }
  143. <button
  144. className = 'button-control button-control_light'
  145. disabled = { !isEnabled }
  146. onClick = { this._onClick }
  147. type = 'button'>
  148. Copy
  149. </button>
  150. </div>
  151. </div>
  152. );
  153. }
  154. /**
  155. * Creates a {@code StatelessDropdownMenu} instance.
  156. *
  157. * @param {Array} items - The content to display within the dropdown.
  158. * @param {string} triggerText - The text to display within the
  159. * trigger element.
  160. * @returns {ReactElement}
  161. */
  162. _createDropdownMenu(items, triggerText) {
  163. return (
  164. <StatelessDropdownMenu
  165. isOpen = { this.state.isDropdownOpen }
  166. items = { [ { items } ] }
  167. onItemActivated = { this._onSelect }
  168. onOpenChange = { this._onOpenChange }
  169. shouldFitContainer = { true }>
  170. { this._createDropdownTrigger(triggerText) }
  171. </StatelessDropdownMenu>
  172. );
  173. }
  174. /**
  175. * Creates a React {@code Component} with a redonly HTMLInputElement as a
  176. * trigger for displaying the dropdown menu. The {@code Component} will also
  177. * display the currently selected number.
  178. *
  179. * @param {string} triggerText - Text to display in the HTMLInputElement.
  180. * @private
  181. * @returns {ReactElement}
  182. */
  183. _createDropdownTrigger(triggerText) {
  184. return (
  185. <div className = 'dial-in-numbers-trigger'>
  186. <input
  187. className = 'input-control'
  188. readOnly = { true }
  189. ref = { this._setInput }
  190. type = 'text'
  191. value = { triggerText || '' } />
  192. <span className = 'dial-in-numbers-trigger-icon'>
  193. { EXPAND_ICON }
  194. </span>
  195. </div>
  196. );
  197. }
  198. /**
  199. * Detects whether the response from dialInNumbersUrl returned an array or
  200. * an object with dial-in numbers and calls the appropriate method to
  201. * transform the numbers into the format expected by
  202. * {@code StatelessDropdownMenu}.
  203. *
  204. * @param {Array<string>|Object} dialInNumbers - The numbers returned from
  205. * requesting dialInNumbersUrl.
  206. * @private
  207. * @returns {Array<Object>}
  208. */
  209. _formatNumbers(dialInNumbers) {
  210. if (Array.isArray(dialInNumbers)) {
  211. return this._formatNumbersArray(dialInNumbers);
  212. }
  213. return this._formatNumbersObject(dialInNumbers);
  214. }
  215. /**
  216. * Transforms the passed in numbers array into an array of objects that can
  217. * be parsed by {@code StatelessDropdownMenu}.
  218. *
  219. * @param {Array<string>} dialInNumbers - An array with dial-in numbers to
  220. * display and copy.
  221. * @private
  222. * @returns {Array<Object>}
  223. */
  224. _formatNumbersArray(dialInNumbers) {
  225. return dialInNumbers.map(number => {
  226. return {
  227. content: number,
  228. number
  229. };
  230. });
  231. }
  232. /**
  233. * Transforms the passed in numbers object into an array of objects that can
  234. * be parsed by {@code StatelessDropdownMenu}.
  235. *
  236. * @param {Object} dialInNumbers - The numbers object to parse. The
  237. * expected format is an object with keys being the name of the country
  238. * and the values being an array of numbers as strings.
  239. * @private
  240. * @returns {Array<Object>}
  241. */
  242. _formatNumbersObject(dialInNumbers) {
  243. const phoneRegions = Object.keys(dialInNumbers);
  244. if (!phoneRegions.length) {
  245. return [];
  246. }
  247. const formattedNumbeers = phoneRegions.map(region => {
  248. const numbers = dialInNumbers[region];
  249. return numbers.map(number => {
  250. return {
  251. content: `${region}: ${number}`,
  252. number
  253. };
  254. });
  255. });
  256. return Array.prototype.concat(...formattedNumbeers);
  257. }
  258. /**
  259. * Determines if the dropdown can be opened.
  260. *
  261. * @private
  262. * @returns {boolean} True if the dropdown can be opened.
  263. */
  264. _isDropdownEnabled() {
  265. const { selectedNumber } = this.state;
  266. return Boolean(
  267. this.props._dialIn.numbersEnabled
  268. && selectedNumber
  269. && selectedNumber.content
  270. );
  271. }
  272. /**
  273. * Copies part of the number displayed in the dropdown trigger into the
  274. * clipboard. Only the value specified in selectedNumber.number, which
  275. * should be a substring of the displayed value, will be copied.
  276. *
  277. * @private
  278. * @returns {void}
  279. */
  280. _onClick() {
  281. const displayedValue = this.state.selectedNumber.content;
  282. const desiredNumber = this.state.selectedNumber.number;
  283. const startIndex = displayedValue.indexOf(desiredNumber);
  284. try {
  285. this._input.focus();
  286. this._input.setSelectionRange(startIndex, displayedValue.length);
  287. document.execCommand('copy');
  288. this._input.blur();
  289. } catch (err) {
  290. logger.error('error when copying the text', err);
  291. }
  292. }
  293. /**
  294. * Sets the internal state to either open or close the dropdown. If the
  295. * dropdown is disabled, the state will always be set to false.
  296. *
  297. * @param {Object} dropdownEvent - The even returned from clicking on the
  298. * dropdown trigger.
  299. * @private
  300. * @returns {void}
  301. */
  302. _onOpenChange(dropdownEvent) {
  303. this.setState({
  304. isDropdownOpen: this._isDropdownEnabled() && dropdownEvent.isOpen
  305. });
  306. }
  307. /**
  308. * Updates the internal state of the currently selected number.
  309. *
  310. * @param {Object} selection - Event from choosing an dropdown option.
  311. * @private
  312. * @returns {void}
  313. */
  314. _onSelect(selection) {
  315. this.setState({
  316. isDropdownOpen: false,
  317. selectedNumber: selection.item
  318. });
  319. }
  320. /**
  321. * Updates the internal state of the currently selected number by defaulting
  322. * to the first available number.
  323. *
  324. * @param {Object} dialInNumbers - The array or object of numbers to parse.
  325. * @private
  326. * @returns {void}
  327. */
  328. _setDefaultNumber(dialInNumbers) {
  329. const numbers = this._formatNumbers(dialInNumbers);
  330. this.setState({
  331. selectedNumber: numbers[0]
  332. });
  333. }
  334. /**
  335. * Sets the internal reference to the DOM/HTML element backing the React
  336. * {@code Component} input.
  337. *
  338. * @param {HTMLInputElement} element - The DOM/HTML element for this
  339. * {@code Component}'s input.
  340. * @private
  341. * @returns {void}
  342. */
  343. _setInput(element) {
  344. this._input = element;
  345. }
  346. }
  347. /**
  348. * Maps (parts of) the Redux state to the associated
  349. * {@code DialInNumbersForm}'s props.
  350. *
  351. * @param {Object} state - The Redux state.
  352. * @private
  353. * @returns {{
  354. * _dialIn: React.PropTypes.object
  355. * }}
  356. */
  357. function _mapStateToProps(state) {
  358. return {
  359. _dialIn: state['features/invite/dial-in']
  360. };
  361. }
  362. export default translate(connect(_mapStateToProps)(DialInNumbersForm));