Browse Source

Merge pull request #1531 from virtuacoplenny/lenny/invite-phone-numbers

feat(invite): include dial-in numbers in the invite modal
j8
yanas 8 years ago
parent
commit
1ffa7be4e1

+ 22
- 0
css/modals/invite/_invite.scss View File

@@ -7,6 +7,26 @@
7 7
 }
8 8
 
9 9
 .invite-dialog {
10
+    .dial-in-numbers {
11
+        .dial-in-numbers-trigger {
12
+            position: relative;
13
+            width: 100%;
14
+
15
+            .dial-in-numbers-trigger-icon {
16
+                position: absolute;
17
+                right: 0;
18
+                top: 4px;
19
+            }
20
+        }
21
+
22
+        .is-disabled,
23
+        .is-loading {
24
+            .dial-in-numbers-trigger-icon {
25
+                display: none;
26
+            }
27
+        }
28
+    }
29
+
10 30
     .form-control {
11 31
         padding: 0;
12 32
     }
@@ -20,6 +40,8 @@
20 40
     }
21 41
 
22 42
     .password-overview {
43
+        margin-top: 10px;
44
+
23 45
         .form-control {
24 46
             margin-top: 10px;
25 47
         }

+ 5
- 0
lang/main.json View File

@@ -428,9 +428,14 @@
428 428
     },
429 429
     "invite": {
430 430
         "addPassword": "Add password",
431
+        "dialInNumbers": "Dial-in telephone numbers",
432
+        "errorFetchingNumbers": "Failed to obtain dial-in numbers",
431 433
         "hidePassword": "Hide password",
432 434
         "inviteTo": "Invite people to __conferenceName__",
435
+        "loadingNumbers": "Loading...",
433 436
         "locked": "This call is locked. New callers must have the link and enter the password to join.",
437
+        "noNumbers": "No numbers available",
438
+        "numbersDisabled": "Dialing in has been disabled",
434 439
         "showPassword": "Show password",
435 440
         "unlocked": "This call is unlocked. Any new caller with the link may join the call."
436 441
     }

+ 38
- 0
react/features/invite/actionTypes.js View File

@@ -0,0 +1,38 @@
1
+import { Symbol } from '../base/react';
2
+
3
+/**
4
+ * The type of the action which signals an error occurred while requesting dial-
5
+ * in numbers.
6
+ *
7
+ * {
8
+ *     type: UPDATE_DIAL_IN_NUMBERS_FAILED,
9
+ *     error: Object
10
+ * }
11
+ */
12
+export const UPDATE_DIAL_IN_NUMBERS_FAILED
13
+    = Symbol('UPDATE_DIAL_IN_NUMBERS_FAILED');
14
+
15
+/**
16
+ * The type of the action which signals a request for dial-in numbers has been
17
+ * started.
18
+ *
19
+ * {
20
+ *     type: UPDATE_DIAL_IN_NUMBERS_REQUEST
21
+ * }
22
+ */
23
+export const UPDATE_DIAL_IN_NUMBERS_REQUEST
24
+    = Symbol('UPDATE_DIAL_IN_NUMBERS_REQUEST');
25
+
26
+/**
27
+ * The type of the action which signals a request for dial-in numbers has
28
+ * succeeded.
29
+ *
30
+ * {
31
+ *     type: UPDATE_DIAL_IN_NUMBERS_SUCCESS,
32
+ *     response: Object
33
+ * }
34
+ */
35
+export const UPDATE_DIAL_IN_NUMBERS_SUCCESS
36
+    = Symbol('UPDATE_DIAL_IN_NUMBERS_SUCCESS');
37
+
38
+

+ 38
- 3
react/features/invite/actions.js View File

@@ -1,9 +1,16 @@
1
-/* globals APP */
2
-
3 1
 import { openDialog } from '../../features/base/dialog';
4 2
 
3
+import {
4
+    UPDATE_DIAL_IN_NUMBERS_FAILED,
5
+    UPDATE_DIAL_IN_NUMBERS_REQUEST,
6
+    UPDATE_DIAL_IN_NUMBERS_SUCCESS
7
+} from './actionTypes';
5 8
 import { InviteDialog } from './components';
6 9
 
10
+declare var $: Function;
11
+declare var APP: Object;
12
+declare var config: Object;
13
+
7 14
 /**
8 15
  * Opens the Invite Dialog.
9 16
  *
@@ -11,6 +18,34 @@ import { InviteDialog } from './components';
11 18
  */
12 19
 export function openInviteDialog() {
13 20
     return openDialog(InviteDialog, {
14
-        conferenceUrl: encodeURI(APP.ConferenceUrl.getInviteUrl())
21
+        conferenceUrl: encodeURI(APP.ConferenceUrl.getInviteUrl()),
22
+        dialInNumbersUrl: config.dialInNumbersUrl
15 23
     });
16 24
 }
25
+
26
+/**
27
+ * Sends an ajax request for dial-in numbers.
28
+ *
29
+ * @param {string} dialInNumbersUrl - The endpoint for retrieving json that
30
+ * includes numbers for dialing in to a conference.
31
+ * @returns {Function}
32
+ */
33
+export function updateDialInNumbers(dialInNumbersUrl) {
34
+    return dispatch => {
35
+        dispatch({
36
+            type: UPDATE_DIAL_IN_NUMBERS_REQUEST
37
+        });
38
+
39
+        $.getJSON(dialInNumbersUrl)
40
+            .success(response =>
41
+                dispatch({
42
+                    type: UPDATE_DIAL_IN_NUMBERS_SUCCESS,
43
+                    response
44
+                }))
45
+            .error(error =>
46
+                dispatch({
47
+                    type: UPDATE_DIAL_IN_NUMBERS_FAILED,
48
+                    error
49
+                }));
50
+    };
51
+}

+ 404
- 0
react/features/invite/components/DialInNumbersForm.js View File

@@ -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));

+ 23
- 0
react/features/invite/components/InviteDialog.js View File

@@ -11,6 +11,7 @@ import {
11 11
 
12 12
 import PasswordContainer from './PasswordContainer';
13 13
 import ShareLinkForm from './ShareLinkForm';
14
+import DialInNumbersForm from './DialInNumbersForm';
14 15
 
15 16
 /**
16 17
  * A React {@code Component} for displaying other components responsible for
@@ -40,6 +41,11 @@ class InviteDialog extends Component {
40 41
          */
41 42
         conferenceUrl: React.PropTypes.string,
42 43
 
44
+        /**
45
+         * The url for retrieving dial-in numbers.
46
+         */
47
+        dialInNumbersUrl: React.PropTypes.string,
48
+
43 49
         /**
44 50
          * Invoked to obtain translated strings.
45 51
          */
@@ -75,6 +81,7 @@ class InviteDialog extends Component {
75 81
                 titleString = { titleString }>
76 82
                 <div className = 'invite-dialog'>
77 83
                     <ShareLinkForm toCopy = { this.props.conferenceUrl } />
84
+                    { this._renderDialInNumbersForm() }
78 85
                     <PasswordContainer
79 86
                         conference = { _conference.conference }
80 87
                         locked = { _conference.locked }
@@ -84,6 +91,22 @@ class InviteDialog extends Component {
84 91
             </Dialog>
85 92
         );
86 93
     }
94
+
95
+    /**
96
+     * Creates a React {@code Component} for displaying and copying to clipboard
97
+     * telephone numbers for dialing in to the conference.
98
+     *
99
+     * @private
100
+     * @returns {ReactElement|null}
101
+     */
102
+    _renderDialInNumbersForm() {
103
+        return (
104
+            this.props.dialInNumbersUrl
105
+                ? <DialInNumbersForm
106
+                    dialInNumbersUrl = { this.props.dialInNumbersUrl } />
107
+                : null
108
+        );
109
+    }
87 110
 }
88 111
 
89 112
 /**

+ 2
- 0
react/features/invite/index.js View File

@@ -1,2 +1,4 @@
1 1
 export * from './actions';
2 2
 export * from './components';
3
+
4
+import './reducer';

+ 47
- 0
react/features/invite/reducer.js View File

@@ -0,0 +1,47 @@
1
+import {
2
+    ReducerRegistry
3
+} from '../base/redux';
4
+
5
+import {
6
+    UPDATE_DIAL_IN_NUMBERS_FAILED,
7
+    UPDATE_DIAL_IN_NUMBERS_REQUEST,
8
+    UPDATE_DIAL_IN_NUMBERS_SUCCESS
9
+} from './actionTypes';
10
+
11
+const DEFAULT_STATE = {
12
+    numbersEnabled: true
13
+};
14
+
15
+ReducerRegistry.register(
16
+    'features/invite/dial-in',
17
+    (state = DEFAULT_STATE, action) => {
18
+        switch (action.type) {
19
+        case UPDATE_DIAL_IN_NUMBERS_FAILED: {
20
+            return {
21
+                ...state,
22
+                error: action.error,
23
+                loading: false
24
+            };
25
+        }
26
+
27
+        case UPDATE_DIAL_IN_NUMBERS_REQUEST: {
28
+            return {
29
+                ...state,
30
+                error: null,
31
+                loading: true
32
+            };
33
+        }
34
+        case UPDATE_DIAL_IN_NUMBERS_SUCCESS: {
35
+            const { numbers, numbersEnabled } = action.response;
36
+
37
+            return {
38
+                error: null,
39
+                loading: false,
40
+                numbers,
41
+                numbersEnabled
42
+            };
43
+        }
44
+        }
45
+
46
+        return state;
47
+    });

Loading…
Cancel
Save