瀏覽代碼

feat(invite): Add conference id to dial-in numbers display

DialInNumbersForm has been modified to display a conference id to be used for
dialing into the conference. The changes include:
- Requesting the conference id and adding the conference id to the redux store
- Displaying the conference id in DialInNumbersForm
- Modifying the copy behavior to support copying the new message to clipboard
- DialInNumbersForm does not display until all ajax requests have completed
  successfully. This eliminates the need for the REQUESTING state.
master
Leonard Kim 8 年之前
父節點
當前提交
47c07c2e76

+ 13
- 0
css/modals/invite/_invite.scss 查看文件

8
 
8
 
9
 .invite-dialog {
9
 .invite-dialog {
10
     .dial-in-numbers {
10
     .dial-in-numbers {
11
+        .dial-in-numbers-copy {
12
+            opacity: 0;
13
+            pointer-events: none;
14
+            position: fixed;
15
+            user-select: text;
16
+            -webkit-user-select: text;
17
+        }
18
+
19
+        .dial-in-numbers-conference-id {
20
+            color: orange;
21
+            margin-left: 3px;
22
+        }
23
+
11
         .dial-in-numbers-trigger {
24
         .dial-in-numbers-trigger {
12
             position: relative;
25
             position: relative;
13
             width: 100%;
26
             width: 100%;

+ 4
- 5
lang/main.json 查看文件

437
     },
437
     },
438
     "invite": {
438
     "invite": {
439
         "addPassword": "Add password",
439
         "addPassword": "Add password",
440
-        "dialInNumbers": "Dial-in telephone numbers",
441
-        "errorFetchingNumbers": "Failed to obtain dial-in numbers",
440
+        "callNumber": "Call __number__",
441
+        "enterId": "Enter Meeting ID: __meetingId__ following by # to dial in from a phone",
442
+        "howToDialIn": "To dial in, use one of the following numbers and meeting ID",
442
         "hidePassword": "Hide password",
443
         "hidePassword": "Hide password",
443
         "inviteTo": "Invite people to __conferenceName__",
444
         "inviteTo": "Invite people to __conferenceName__",
444
-        "loadingNumbers": "Loading...",
445
+        "invitedYouTo": "__userName__ has invited you to the __meetingUrl__ conference",
445
         "locked": "This call is locked. New callers must have the link and enter the password to join.",
446
         "locked": "This call is locked. New callers must have the link and enter the password to join.",
446
-        "noNumbers": "No numbers available",
447
-        "numbersDisabled": "Dialing in has been disabled",
448
         "showPassword": "Show password",
447
         "showPassword": "Show password",
449
         "unlocked": "This call is unlocked. Any new caller with the link may join the call."
448
         "unlocked": "This call is unlocked. Any new caller with the link may join the call."
450
     }
449
     }

+ 0
- 11
react/features/invite/actionTypes.js 查看文件

12
 export const UPDATE_DIAL_IN_NUMBERS_FAILED
12
 export const UPDATE_DIAL_IN_NUMBERS_FAILED
13
     = Symbol('UPDATE_DIAL_IN_NUMBERS_FAILED');
13
     = Symbol('UPDATE_DIAL_IN_NUMBERS_FAILED');
14
 
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
 /**
15
 /**
27
  * The type of the action which signals a request for dial-in numbers has
16
  * The type of the action which signals a request for dial-in numbers has
28
  * succeeded.
17
  * succeeded.

+ 36
- 21
react/features/invite/actions.js 查看文件

2
 
2
 
3
 import {
3
 import {
4
     UPDATE_DIAL_IN_NUMBERS_FAILED,
4
     UPDATE_DIAL_IN_NUMBERS_FAILED,
5
-    UPDATE_DIAL_IN_NUMBERS_REQUEST,
6
     UPDATE_DIAL_IN_NUMBERS_SUCCESS
5
     UPDATE_DIAL_IN_NUMBERS_SUCCESS
7
 } from './actionTypes';
6
 } from './actionTypes';
8
 import { InviteDialog } from './components';
7
 import { InviteDialog } from './components';
11
 declare var APP: Object;
10
 declare var APP: Object;
12
 declare var config: Object;
11
 declare var config: Object;
13
 
12
 
13
+const CONFERENCE_ID_ENDPOINT = config.conferenceMapperUrl;
14
+const DIAL_IN_NUMBERS_ENDPOINT = config.dialInNumbersUrl;
15
+const MUC_URL = config && config.hosts && config.hosts.muc;
16
+
14
 /**
17
 /**
15
  * Opens the Invite Dialog.
18
  * Opens the Invite Dialog.
16
  *
19
  *
18
  */
21
  */
19
 export function openInviteDialog() {
22
 export function openInviteDialog() {
20
     return openDialog(InviteDialog, {
23
     return openDialog(InviteDialog, {
21
-        conferenceUrl: encodeURI(APP.ConferenceUrl.getInviteUrl()),
22
-        dialInNumbersUrl: config.dialInNumbersUrl
24
+        conferenceUrl: encodeURI(APP.ConferenceUrl.getInviteUrl())
23
     });
25
     });
24
 }
26
 }
25
 
27
 
26
 /**
28
 /**
27
- * Sends an ajax request for dial-in numbers.
29
+ * Sends an ajax requests for dial-in numbers and conference id.
28
  *
30
  *
29
- * @param {string} dialInNumbersUrl - The endpoint for retrieving json that
30
- * includes numbers for dialing in to a conference.
31
  * @returns {Function}
31
  * @returns {Function}
32
  */
32
  */
33
-export function updateDialInNumbers(dialInNumbersUrl) {
34
-    return dispatch => {
35
-        dispatch({
36
-            type: UPDATE_DIAL_IN_NUMBERS_REQUEST
33
+export function updateDialInNumbers() {
34
+    return (dispatch, getState) => {
35
+
36
+        if (!CONFERENCE_ID_ENDPOINT || !DIAL_IN_NUMBERS_ENDPOINT || !MUC_URL) {
37
+            return;
38
+        }
39
+
40
+        const { room } = getState()['features/base/conference'];
41
+        const conferenceIdUrl
42
+            = `${CONFERENCE_ID_ENDPOINT}?conference=${room}@${MUC_URL}`;
43
+
44
+        Promise.all([
45
+            $.getJSON(DIAL_IN_NUMBERS_ENDPOINT),
46
+            $.getJSON(conferenceIdUrl)
47
+        ]).then(([ numbersResponse, idResponse ]) => {
48
+            if (!idResponse.conference || !idResponse.id) {
49
+                return Promise.reject(idResponse.message);
50
+            }
51
+
52
+            dispatch({
53
+                type: UPDATE_DIAL_IN_NUMBERS_SUCCESS,
54
+                conferenceId: idResponse,
55
+                dialInNumbers: numbersResponse
56
+            });
57
+        })
58
+        .catch(error => {
59
+            dispatch({
60
+                type: UPDATE_DIAL_IN_NUMBERS_FAILED,
61
+                error
62
+            });
37
         });
63
         });
38
 
64
 
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
     };
65
     };
51
 }
66
 }

+ 78
- 75
react/features/invite/components/DialInNumbersForm.js 查看文件

4
 import { connect } from 'react-redux';
4
 import { connect } from 'react-redux';
5
 
5
 
6
 import { translate } from '../../base/i18n';
6
 import { translate } from '../../base/i18n';
7
+import { getLocalParticipant } from '../../base/participants';
7
 
8
 
8
 import { updateDialInNumbers } from '../actions';
9
 import { updateDialInNumbers } from '../actions';
9
 
10
 
25
      * @static
26
      * @static
26
      */
27
      */
27
     static propTypes = {
28
     static propTypes = {
29
+        /**
30
+         * The name of the current user.
31
+         */
32
+        _currentUserName: React.PropTypes.string,
33
+
28
         /**
34
         /**
29
          * The redux state representing the dial-in numbers feature.
35
          * The redux state representing the dial-in numbers feature.
30
          */
36
          */
31
         _dialIn: React.PropTypes.object,
37
         _dialIn: React.PropTypes.object,
32
 
38
 
33
         /**
39
         /**
34
-         * The url for retrieving dial-in numbers.
40
+         * The url for the JitsiConference.
35
          */
41
          */
36
-        dialInNumbersUrl: React.PropTypes.string,
42
+        conferenceUrl: React.PropTypes.string,
37
 
43
 
38
         /**
44
         /**
39
          * Invoked to send an ajax request for dial-in numbers.
45
          * Invoked to send an ajax request for dial-in numbers.
77
 
83
 
78
         /**
84
         /**
79
          * The internal reference to the DOM/HTML element backing the React
85
          * 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.
86
+         * {@code Component} text area. It is necessary for the implementation
87
+         * of copying to the clipboard.
82
          *
88
          *
83
          * @private
89
          * @private
84
-         * @type {HTMLInputElement}
90
+         * @type {HTMLTextAreaElement}
85
          */
91
          */
86
-        this._inputElement = null;
92
+        this._copyElement = null;
87
 
93
 
88
         // Bind event handlers so they are only bound once for every instance.
94
         // Bind event handlers so they are only bound once for every instance.
89
-        this._onClick = this._onClick.bind(this);
95
+        this._onCopyClick = this._onCopyClick.bind(this);
90
         this._onOpenChange = this._onOpenChange.bind(this);
96
         this._onOpenChange = this._onOpenChange.bind(this);
91
         this._onSelect = this._onSelect.bind(this);
97
         this._onSelect = this._onSelect.bind(this);
92
-        this._setInput = this._setInput.bind(this);
98
+        this._setCopyElement = this._setCopyElement.bind(this);
93
     }
99
     }
94
 
100
 
95
     /**
101
     /**
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.
102
+     * Sets a default number to display in the dropdown trigger.
99
      *
103
      *
100
      * @inheritdoc
104
      * @inheritdoc
101
      * returns {void}
105
      * returns {void}
102
      */
106
      */
103
-    componentDidMount() {
107
+    componentWillMount() {
104
         if (this.props._dialIn.numbers) {
108
         if (this.props._dialIn.numbers) {
105
             this._setDefaultNumber(this.props._dialIn.numbers);
109
             this._setDefaultNumber(this.props._dialIn.numbers);
106
         } else {
110
         } else {
107
-            this.props.dispatch(
108
-                updateDialInNumbers(this.props.dialInNumbersUrl));
111
+            this.props.dispatch(updateDialInNumbers());
109
         }
112
         }
110
     }
113
     }
111
 
114
 
123
     }
126
     }
124
 
127
 
125
     /**
128
     /**
126
-     * Implements React's {@link Component#render()}.
129
+     * Implements React's {@link Component#render()}. Returns null if the
130
+     * component is not ready for display.
127
      *
131
      *
128
      * @inheritdoc
132
      * @inheritdoc
129
-     * @returns {ReactElement}
133
+     * @returns {ReactElement|null}
130
      */
134
      */
131
     render() {
135
     render() {
132
-        const { t, _dialIn } = this.props;
133
-
134
-        const numbers = _dialIn.numbers;
135
-        const items = numbers ? this._formatNumbers(numbers) : [];
136
+        const { _dialIn, t } = this.props;
137
+        const { conferenceId, numbers, numbersEnabled } = _dialIn;
138
+        const { selectedNumber } = this.state;
136
 
139
 
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');
140
+        if (!conferenceId || !numbers || !numbersEnabled || !selectedNumber) {
141
+            return null;
155
         }
142
         }
156
 
143
 
144
+        const items = numbers ? this._formatNumbers(numbers) : [];
145
+
157
         return (
146
         return (
158
             <div className = 'form-control dial-in-numbers'>
147
             <div className = 'form-control dial-in-numbers'>
159
                 <label className = 'form-control__label'>
148
                 <label className = 'form-control__label'>
160
-                    { t('invite.dialInNumbers') }
149
+                    { t('invite.howToDialIn') }
150
+                    <span className = 'dial-in-numbers-conference-id'>
151
+                        { conferenceId }
152
+                    </span>
161
                 </label>
153
                 </label>
162
-                <div className = { inputWrapperClassNames }>
163
-                    { this._createDropdownMenu(items, triggerText) }
154
+                <div className = 'form-control__container'>
155
+                    { this._createDropdownMenu(items, selectedNumber.content) }
164
                     <button
156
                     <button
165
                         className = 'button-control button-control_light'
157
                         className = 'button-control button-control_light'
166
-                        disabled = { !isEnabled }
167
-                        onClick = { this._onClick }
158
+                        onClick = { this._onCopyClick }
168
                         type = 'button'>
159
                         type = 'button'>
169
                         Copy
160
                         Copy
170
                     </button>
161
                     </button>
171
                 </div>
162
                 </div>
163
+                <textarea
164
+                    className = 'dial-in-numbers-copy'
165
+                    readOnly = { true }
166
+                    ref = { this._setCopyElement }
167
+                    tabIndex = '-1'
168
+                    value = { this._generateCopyText() } />
172
             </div>
169
             </div>
173
         );
170
         );
174
     }
171
     }
209
                 <input
206
                 <input
210
                     className = 'input-control'
207
                     className = 'input-control'
211
                     readOnly = { true }
208
                     readOnly = { true }
212
-                    ref = { this._setInput }
213
                     type = 'text'
209
                     type = 'text'
214
                     value = { triggerText || '' } />
210
                     value = { triggerText || '' } />
215
                 <span className = 'dial-in-numbers-trigger-icon'>
211
                 <span className = 'dial-in-numbers-trigger-icon'>
288
     }
284
     }
289
 
285
 
290
     /**
286
     /**
291
-     * Determines if the dropdown can be opened.
287
+     * Creates a message describing how to dial in to the conference.
292
      *
288
      *
293
      * @private
289
      * @private
294
-     * @returns {boolean} True if the dropdown can be opened.
290
+     * @returns {string}
295
      */
291
      */
296
-    _isDropdownEnabled() {
297
-        const { selectedNumber } = this.state;
292
+    _generateCopyText() {
293
+        const welcome = this.props.t('invite.invitedYouTo', {
294
+            meetingUrl: this.props.conferenceUrl,
295
+            userName: this.props._currentUserName
296
+        });
298
 
297
 
299
-        return Boolean(
300
-            this.props._dialIn.numbersEnabled
301
-            && selectedNumber
302
-            && selectedNumber.content
303
-        );
298
+        const callNumber = this.props.t('invite.callNumber',
299
+            { number: this.state.selectedNumber.number });
300
+        const stepOne = `1) ${callNumber}`;
301
+
302
+        const enterId = this.props.t('invite.enterId',
303
+            { meetingId: this.props._dialIn.conferenceId });
304
+        const stepTwo = `2) ${enterId}`;
305
+
306
+        return `${welcome}\n${stepOne}\n${stepTwo}`;
304
     }
307
     }
305
 
308
 
306
     /**
309
     /**
311
      * @private
314
      * @private
312
      * @returns {void}
315
      * @returns {void}
313
      */
316
      */
314
-    _onClick() {
315
-        const displayedValue = this.state.selectedNumber.content;
316
-        const desiredNumber = this.state.selectedNumber.number;
317
-        const startIndex = displayedValue.indexOf(desiredNumber);
318
-
317
+    _onCopyClick() {
319
         try {
318
         try {
320
-            this._input.focus();
321
-            this._input.setSelectionRange(startIndex, displayedValue.length);
319
+            this._copyElement.select();
322
             document.execCommand('copy');
320
             document.execCommand('copy');
323
-            this._input.blur();
321
+            this._copyElement.blur();
324
         } catch (err) {
322
         } catch (err) {
325
             logger.error('error when copying the text', err);
323
             logger.error('error when copying the text', err);
326
         }
324
         }
337
      */
335
      */
338
     _onOpenChange(dropdownEvent) {
336
     _onOpenChange(dropdownEvent) {
339
         this.setState({
337
         this.setState({
340
-            isDropdownOpen: this._isDropdownEnabled() && dropdownEvent.isOpen
338
+            isDropdownOpen: dropdownEvent.isOpen
341
         });
339
         });
342
     }
340
     }
343
 
341
 
355
         });
353
         });
356
     }
354
     }
357
 
355
 
356
+    /**
357
+     * Sets the internal reference to the DOM/HTML element backing the React
358
+     * {@code Component} text area.
359
+     *
360
+     * @param {HTMLTextAreaElement} element - The DOM/HTML element for this
361
+     * {@code Component}'s text area.
362
+     * @private
363
+     * @returns {void}
364
+     */
365
+    _setCopyElement(element) {
366
+        this._copyElement = element;
367
+    }
368
+
358
     /**
369
     /**
359
      * Updates the internal state of the currently selected number by defaulting
370
      * Updates the internal state of the currently selected number by defaulting
360
      * to the first available number.
371
      * to the first available number.
370
             selectedNumber: numbers[0]
381
             selectedNumber: numbers[0]
371
         });
382
         });
372
     }
383
     }
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
 }
384
 }
387
 
385
 
388
 /**
386
 /**
392
  * @param {Object} state - The Redux state.
390
  * @param {Object} state - The Redux state.
393
  * @private
391
  * @private
394
  * @returns {{
392
  * @returns {{
393
+ *     _currentUserName: React.PropTypes.string,
395
  *     _dialIn: React.PropTypes.object
394
  *     _dialIn: React.PropTypes.object
396
  * }}
395
  * }}
397
  */
396
  */
398
 function _mapStateToProps(state) {
397
 function _mapStateToProps(state) {
398
+    const { name }
399
+        = getLocalParticipant(state['features/base/participants']);
400
+
399
     return {
401
     return {
402
+        _currentUserName: name,
400
         _dialIn: state['features/invite/dial-in']
403
         _dialIn: state['features/invite/dial-in']
401
     };
404
     };
402
 }
405
 }

+ 3
- 24
react/features/invite/components/InviteDialog.js 查看文件

41
          */
41
          */
42
         conferenceUrl: React.PropTypes.string,
42
         conferenceUrl: React.PropTypes.string,
43
 
43
 
44
-        /**
45
-         * The url for retrieving dial-in numbers.
46
-         */
47
-        dialInNumbersUrl: React.PropTypes.string,
48
-
49
         /**
44
         /**
50
          * Invoked to obtain translated strings.
45
          * Invoked to obtain translated strings.
51
          */
46
          */
68
      * @returns {ReactElement}
63
      * @returns {ReactElement}
69
      */
64
      */
70
     render() {
65
     render() {
71
-        const { _conference } = this.props;
66
+        const { _conference, conferenceUrl } = this.props;
72
         const titleString
67
         const titleString
73
             = this.props.t(
68
             = this.props.t(
74
                 'invite.inviteTo',
69
                 'invite.inviteTo',
80
                 okTitleKey = 'dialog.done'
75
                 okTitleKey = 'dialog.done'
81
                 titleString = { titleString }>
76
                 titleString = { titleString }>
82
                 <div className = 'invite-dialog'>
77
                 <div className = 'invite-dialog'>
83
-                    <ShareLinkForm toCopy = { this.props.conferenceUrl } />
84
-                    { this._renderDialInNumbersForm() }
78
+                    <ShareLinkForm toCopy = { conferenceUrl } />
79
+                    <DialInNumbersForm conferenceUrl = { conferenceUrl } />
85
                     <PasswordContainer
80
                     <PasswordContainer
86
                         conference = { _conference.conference }
81
                         conference = { _conference.conference }
87
                         locked = { _conference.locked }
82
                         locked = { _conference.locked }
91
             </Dialog>
86
             </Dialog>
92
         );
87
         );
93
     }
88
     }
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
-    }
110
 }
89
 }
111
 
90
 
112
 /**
91
 /**

+ 3
- 12
react/features/invite/reducer.js 查看文件

4
 
4
 
5
 import {
5
 import {
6
     UPDATE_DIAL_IN_NUMBERS_FAILED,
6
     UPDATE_DIAL_IN_NUMBERS_FAILED,
7
-    UPDATE_DIAL_IN_NUMBERS_REQUEST,
8
     UPDATE_DIAL_IN_NUMBERS_SUCCESS
7
     UPDATE_DIAL_IN_NUMBERS_SUCCESS
9
 } from './actionTypes';
8
 } from './actionTypes';
10
 
9
 
19
         case UPDATE_DIAL_IN_NUMBERS_FAILED: {
18
         case UPDATE_DIAL_IN_NUMBERS_FAILED: {
20
             return {
19
             return {
21
                 ...state,
20
                 ...state,
22
-                error: action.error,
23
-                loading: false
21
+                error: action.error
24
             };
22
             };
25
         }
23
         }
26
 
24
 
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: {
25
         case UPDATE_DIAL_IN_NUMBERS_SUCCESS: {
35
-            const { numbers, numbersEnabled } = action.response;
26
+            const { numbers, numbersEnabled } = action.dialInNumbers;
36
 
27
 
37
             return {
28
             return {
29
+                conferenceId: action.conferenceId.id,
38
                 error: null,
30
                 error: null,
39
-                loading: false,
40
                 numbers,
31
                 numbers,
41
                 numbersEnabled
32
                 numbersEnabled
42
             };
33
             };

Loading…
取消
儲存