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