Selaa lähdekoodia

feat(prejoin): Add precall connection quality indicator

* Adds a dropdown indicator which displays the status of the internet connection.
* It uses the same data as `https://network.callstats.io`.
* The algorithm for the strings displayed to the user is also the one used on `network.callstas.io`.
master
Vlad Piersec 4 vuotta sitten
vanhempi
commit
453c07cb17

+ 4
- 1
conference.js Näytä tiedosto

@@ -121,7 +121,8 @@ import { suspendDetected } from './react/features/power-monitor';
121 121
 import {
122 122
     initPrejoin,
123 123
     isPrejoinPageEnabled,
124
-    isPrejoinPageVisible
124
+    isPrejoinPageVisible,
125
+    makePrecallTest
125 126
 } from './react/features/prejoin';
126 127
 import { createRnnoiseProcessorPromise } from './react/features/rnnoise';
127 128
 import { toggleScreenshotCaptureEffect } from './react/features/screenshot-capture';
@@ -767,6 +768,8 @@ export default {
767 768
                 return c;
768 769
             });
769 770
 
771
+            APP.store.dispatch(makePrecallTest(this._getConferenceOptions()));
772
+
770 773
             const { tryCreateLocalTracks, errors } = this.createInitialLocalTracks(initialOptions);
771 774
             const tracks = await tryCreateLocalTracks;
772 775
 

+ 60
- 0
css/_connection-status.scss Näytä tiedosto

@@ -0,0 +1,60 @@
1
+.con-status {
2
+    position: absolute;
3
+    top: 40px;
4
+    width: 100%;
5
+    z-index: $toolbarZ + 3;
6
+
7
+    &-container {
8
+        background: rgba(28, 32, 37, .5);
9
+        border-radius: 3px;
10
+        color: #fff;
11
+        font-size: 13px;
12
+        line-height: 20px;
13
+        margin: 0 auto;
14
+        width: 304px;
15
+    }
16
+
17
+    &-header {
18
+        align-items: center;
19
+        display: flex;
20
+        justify-content: space-between;
21
+        padding: 8px;
22
+    }
23
+
24
+    &-circle {
25
+        border-radius: 50%;
26
+        display: inline-block;
27
+        padding: 4px;
28
+    }
29
+
30
+    &--good {
31
+        background: #31B76A;
32
+    }
33
+
34
+    &--poor {
35
+        background: #E12D2D;
36
+    }
37
+
38
+    &--non-optimal {
39
+        background: #E39623;
40
+    }
41
+
42
+    &-arrow {
43
+        &--up {
44
+            transform: rotate(180deg);
45
+        }
46
+
47
+        &>svg {
48
+            cursor: pointer;
49
+        }
50
+    }
51
+
52
+    &-text {
53
+        text-align: center;
54
+    }
55
+
56
+    &-details {
57
+        border-top: 1px solid #5E6D7A;
58
+        padding: 16px;
59
+    }
60
+}

+ 1
- 0
css/main.scss Näytä tiedosto

@@ -102,5 +102,6 @@ $flagsImagePath: "../images/";
102 102
 @import 'premeeting-screens';
103 103
 @import 'e2ee';
104 104
 @import 'responsive';
105
+@import 'connection-status';
105 106
 
106 107
 /* Modules END */

+ 19
- 0
lang/main.json Näytä tiedosto

@@ -511,6 +511,25 @@
511 511
         "callMeAtNumber": "Call me at this number:",
512 512
         "configuringDevices": "Configuring devices...",
513 513
         "connectedWithAudioQ": "You’re connected with audio?",
514
+        "connection": {
515
+           "good": "Your internet connection looks good!",
516
+           "nonOptimal": "Your internet connection is not optimal",
517
+           "poor": "You have a poor internet connection"
518
+        },
519
+        "connectionDetails": {
520
+          "audioClipping": "We expect your audio to be clipped.",
521
+          "audioHighQuality": "We expect your audio to have excellent quality.",
522
+          "audioLowNoVideo": "We expect your audio quality to be low and no video.",
523
+          "goodQuality": "Awesome! Your media quality is going to be great.",
524
+          "noMediaConnectivity": "We could not find a way to establish media connectivity for this test. This is typically caused by a firewall or NAT.",
525
+          "noVideo": "We expect that your video will be terrible.",
526
+          "undetectable": "If you still can not make calls in browser, we recommend that you make sure your speakers, microphone and camera are properly set up, that you have granted your browser rights to use your microphone and camera, and that your browser version is up-to-date. If you still have trouble calling, you should contact the web application developer.",
527
+          "veryPoorConnection": "We expect your call quality to be really terrible.",
528
+          "videoFreezing": "We expect your video to freeze, turn black, and be pixelated.",
529
+          "videoHighQuality": "We expect your video to have good quality.",
530
+          "videoLowQuality": "We expect your video to have low quality in terms of frame rate and resolution.",
531
+          "videoTearing": "We expect your video to be pixelated or have visual artefacts."
532
+        },
514 533
         "copyAndShare": "Copy & share meeting link",
515 534
         "dialInMeeting": "Dial into the meeting",
516 535
         "dialInPin": "Dial into the meeting and enter PIN code:",

+ 3
- 0
react/features/base/icons/svg/index.js Näytä tiedosto

@@ -98,4 +98,7 @@ export { default as IconVolume } from './volume.svg';
98 98
 export { default as IconVolumeEmpty } from './volume-empty.svg';
99 99
 export { default as IconVolumeOff } from './volume-off.svg';
100 100
 export { default as IconWarning } from './warning.svg';
101
+export { default as IconWifi1Bar } from './wifi-1.svg';
102
+export { default as IconWifi2Bars } from './wifi-2.svg';
103
+export { default as IconWifi3Bars } from './wifi-3.svg';
101 104
 export { default as IconYahoo } from './yahoo.svg';

+ 5
- 0
react/features/base/icons/svg/wifi-1.svg Näytä tiedosto

@@ -0,0 +1,5 @@
1
+<svg width="16" height="12" viewBox="0 0 16 12" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+<path opacity="0.4" d="M13.0913 6.59847C12.4227 5.88894 11.629 5.32611 10.7554 4.94212C9.88182 4.55812 8.94553 4.36048 7.99997 4.36048C7.05442 4.36048 6.11813 4.55812 5.24456 4.94212C4.371 5.32611 3.57726 5.88894 2.90869 6.59847L4.36305 8.14176C4.84061 7.63486 5.4076 7.23276 6.03163 6.95842C6.65566 6.68408 7.32451 6.54288 7.99997 6.54288C8.67544 6.54288 9.34429 6.68408 9.96832 6.95842C10.5923 7.23276 11.1593 7.63486 11.6369 8.14176L13.0913 6.59847Z" fill="white"/>
3
+<path opacity="0.4" d="M16 3.51081C13.8766 1.26261 10.9996 0 8 0C5.00044 0 2.12337 1.26261 0 3.51081L1.45436 5.0541C3.19156 3.21432 5.54565 2.18105 8 2.18105C10.4543 2.18105 12.8084 3.21432 14.5456 5.0541L16 3.51081Z" fill="white"/>
4
+<path d="M5.94287 9.81713L7.99996 12L10.057 9.81713C9.78693 9.53041 9.46623 9.30297 9.11328 9.1478C8.76032 8.99263 8.38201 8.91276 7.99996 8.91276C7.6179 8.91276 7.23959 8.99263 6.88663 9.1478C6.53368 9.30297 6.21298 9.53041 5.94287 9.81713Z" />
5
+</svg>

+ 5
- 0
react/features/base/icons/svg/wifi-2.svg Näytä tiedosto

@@ -0,0 +1,5 @@
1
+<svg width="16" height="12" viewBox="0 0 16 12" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+<path d="M13.0913 6.59847C12.4227 5.88894 11.629 5.32611 10.7554 4.94211C9.88182 4.55811 8.94553 4.36047 7.99997 4.36047C7.05442 4.36047 6.11813 4.55811 5.24456 4.94211C4.371 5.32611 3.57726 5.88894 2.90869 6.59847L4.36305 8.14176C4.84061 7.63486 5.4076 7.23276 6.03163 6.95842C6.65566 6.68408 7.32451 6.54288 7.99997 6.54288C8.67544 6.54288 9.34429 6.68408 9.96832 6.95842C10.5923 7.23276 11.1593 7.63486 11.6369 8.14176L13.0913 6.59847Z" fill="white"/>
3
+<path opacity="0.4" d="M16 3.51081C13.8766 1.26261 10.9996 0 8 0C5.00044 0 2.12337 1.26261 0 3.51081L1.45436 5.0541C3.19156 3.21432 5.54565 2.18105 8 2.18105C10.4543 2.18105 12.8084 3.21432 14.5456 5.0541L16 3.51081Z" fill="white"/>
4
+<path d="M5.94287 9.81713L7.99996 12L10.057 9.81713C9.78693 9.53042 9.46623 9.30298 9.11328 9.14781C8.76032 8.99263 8.38201 8.91277 7.99996 8.91277C7.6179 8.91277 7.23959 8.99263 6.88663 9.14781C6.53368 9.30298 6.21298 9.53042 5.94287 9.81713Z" />
5
+</svg>

+ 5
- 0
react/features/base/icons/svg/wifi-3.svg Näytä tiedosto

@@ -0,0 +1,5 @@
1
+<svg width="16" height="12" viewBox="0 0 16 12" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+<path d="M13.0913 6.59847C12.4227 5.88894 11.629 5.32611 10.7554 4.94211C9.88182 4.55811 8.94553 4.36047 7.99997 4.36047C7.05442 4.36047 6.11813 4.55811 5.24456 4.94211C4.371 5.32611 3.57726 5.88894 2.90869 6.59847L4.36305 8.14176C4.84061 7.63486 5.4076 7.23276 6.03163 6.95842C6.65566 6.68408 7.32451 6.54288 7.99997 6.54288C8.67544 6.54288 9.34429 6.68408 9.96832 6.95842C10.5923 7.23276 11.1593 7.63486 11.6369 8.14176L13.0913 6.59847Z" fill="white"/>
3
+<path d="M16 3.51081C13.8766 1.26261 10.9996 0 8 0C5.00044 0 2.12337 1.26261 0 3.51081L1.45436 5.0541C3.19156 3.21432 5.54565 2.18105 8 2.18105C10.4543 2.18105 12.8084 3.21432 14.5456 5.0541L16 3.51081Z" fill="white"/>
4
+<path d="M5.94287 9.81713L7.99996 12L10.057 9.81713C9.78693 9.53042 9.46623 9.30298 9.11328 9.14781C8.76032 8.99263 8.38201 8.91277 7.99996 8.91277C7.6179 8.91277 7.23959 8.99263 6.88663 9.14781C6.53368 9.30298 6.21298 9.53042 5.94287 9.81713Z" />
5
+</svg>

+ 104
- 0
react/features/base/premeeting/components/web/ConnectionStatus.js Näytä tiedosto

@@ -0,0 +1,104 @@
1
+// @flow
2
+
3
+import React, { useState } from 'react';
4
+
5
+import { translate } from '../../../i18n';
6
+import { Icon, IconArrowDownSmall, IconWifi1Bar, IconWifi2Bars, IconWifi3Bars } from '../../../icons';
7
+import { connect } from '../../../redux';
8
+import { CONNECTION_TYPE } from '../../constants';
9
+import { getConnectionData } from '../../functions';
10
+
11
+type Props = {
12
+
13
+    /**
14
+     * List of strings with details about the connection.
15
+     */
16
+    connectionDetails: string[],
17
+
18
+    /**
19
+     * The type of the connection. Can be: 'none', 'poor', 'nonOptimal' or 'good'.
20
+     */
21
+    connectionType: string,
22
+
23
+    /**
24
+     * Used for translation.
25
+     */
26
+    t: Function
27
+}
28
+
29
+const CONNECTION_TYPE_MAP = {
30
+    [CONNECTION_TYPE.POOR]: {
31
+        connectionClass: 'con-status--poor',
32
+        icon: IconWifi1Bar,
33
+        connectionText: 'prejoin.connection.poor'
34
+    },
35
+    [CONNECTION_TYPE.NON_OPTIMAL]: {
36
+        connectionClass: 'con-status--non-optimal',
37
+        icon: IconWifi2Bars,
38
+        connectionText: 'prejoin.connection.nonOptimal'
39
+    },
40
+    [CONNECTION_TYPE.GOOD]: {
41
+        connectionClass: 'con-status--good',
42
+        icon: IconWifi3Bars,
43
+        connectionText: 'prejoin.connection.good'
44
+    }
45
+};
46
+
47
+/**
48
+ * Component displaying information related to the connection & audio/video quality.
49
+ *
50
+ * @param {Props} props - The props of the component.
51
+ * @returns {ReactElement}
52
+ */
53
+function ConnectionStatus({ connectionDetails, t, connectionType }: Props) {
54
+    if (connectionType === CONNECTION_TYPE.NONE) {
55
+        return null;
56
+    }
57
+
58
+    const { connectionClass, icon, connectionText } = CONNECTION_TYPE_MAP[connectionType];
59
+    const [ showDetails, toggleDetails ] = useState(false);
60
+    const arrowClassName = showDetails
61
+        ? 'con-status-arrow con-status-arrow--up'
62
+        : 'con-status-arrow';
63
+    const detailsText = connectionDetails.map(t).join(' ');
64
+
65
+    return (
66
+        <div className = 'con-status'>
67
+            <div className = 'con-status-container'>
68
+                <div className = 'con-status-header'>
69
+                    <div className = { `con-status-circle ${connectionClass}` }>
70
+                        <Icon
71
+                            size = { 16 }
72
+                            src = { icon } />
73
+                    </div>
74
+                    <span className = 'con-status-text'>{t(connectionText)}</span>
75
+                    <Icon
76
+                        className = { arrowClassName }
77
+                        // eslint-disable-next-line react/jsx-no-bind
78
+                        onClick = { () => toggleDetails(!showDetails) }
79
+                        size = { 24 }
80
+                        src = { IconArrowDownSmall } />
81
+                </div>
82
+                { showDetails
83
+                  && <div className = 'con-status-details'>{detailsText}</div> }
84
+            </div>
85
+        </div>
86
+    );
87
+}
88
+
89
+/**
90
+ * Maps (parts of) the redux state to the React {@code Component} props.
91
+ *
92
+ * @param {Object} state - The redux state.
93
+ * @returns {Object}
94
+ */
95
+function mapStateToProps(state): Object {
96
+    const { connectionDetails, connectionType } = getConnectionData(state);
97
+
98
+    return {
99
+        connectionDetails,
100
+        connectionType
101
+    };
102
+}
103
+
104
+export default translate(connect(mapStateToProps)(ConnectionStatus));

+ 2
- 0
react/features/base/premeeting/components/web/PreMeetingScreen.js Näytä tiedosto

@@ -4,6 +4,7 @@ import React, { PureComponent } from 'react';
4 4
 
5 5
 import { AudioSettingsButton, VideoSettingsButton } from '../../../../toolbox/components/web';
6 6
 
7
+import ConnectionStatus from './ConnectionStatus';
7 8
 import CopyMeetingUrl from './CopyMeetingUrl';
8 9
 import Preview from './Preview';
9 10
 
@@ -82,6 +83,7 @@ export default class PreMeetingScreen extends PureComponent<Props> {
82 83
             <div
83 84
                 className = 'premeeting-screen'
84 85
                 id = 'lobby-screen'>
86
+                <ConnectionStatus />
85 87
                 <Preview
86 88
                     name = { name }
87 89
                     showAvatar = { showAvatar }

+ 8
- 0
react/features/base/premeeting/constants.js Näytä tiedosto

@@ -0,0 +1,8 @@
1
+// @flow
2
+
3
+export const CONNECTION_TYPE = {
4
+    GOOD: 'good',
5
+    NON_OPTIMAL: 'nonOptimal',
6
+    NONE: 'none',
7
+    POOR: 'poor'
8
+};

+ 142
- 0
react/features/base/premeeting/functions.js Näytä tiedosto

@@ -0,0 +1,142 @@
1
+import { findIndex } from 'lodash';
2
+
3
+import { CONNECTION_TYPE } from './constants';
4
+
5
+const LOSS_AUDIO_THRESHOLDS = [ 0.33, 0.05 ];
6
+const LOSS_VIDEO_THRESHOLDS = [ 0.33, 0.1, 0.05 ];
7
+
8
+const THROUGHPUT_AUDIO_THRESHOLDS = [ 8, 20 ];
9
+const THROUGHPUT_VIDEO_THRESHOLDS = [ 60, 750 ];
10
+
11
+/**
12
+ * Returns the level based on a list of thresholds.
13
+ *
14
+ * @param {number[]} thresholds - The thresholds array.
15
+ * @param {number} value - The value against which the level is calculated.
16
+ * @param {boolean} descending - The order based on which the level is calculated.
17
+ *
18
+ * @returns {number}
19
+ */
20
+function _getLevel(thresholds, value, descending = true) {
21
+    let predicate;
22
+
23
+    if (descending) {
24
+        predicate = function(threshold) {
25
+            return value > threshold;
26
+        };
27
+    } else {
28
+        predicate = function(threshold) {
29
+            return value < threshold;
30
+        };
31
+    }
32
+
33
+    const i = findIndex(thresholds, predicate);
34
+
35
+    if (i === -1) {
36
+        return thresholds.length;
37
+    }
38
+
39
+    return i;
40
+}
41
+
42
+/**
43
+ * Returns the connection details from the test results.
44
+ *
45
+ * @param {{
46
+ *   fractionalLoss: number,
47
+ *   throughput: number
48
+ * }} testResults - The state of the app.
49
+ *
50
+ * @returns {{
51
+ *   connectionType: string,
52
+ *   connectionDetails: string[]
53
+ * }}
54
+ */
55
+function _getConnectionDataFromTestResults({ fractionalLoss: l, throughput: t }) {
56
+    const loss = {
57
+        audioQuality: _getLevel(LOSS_AUDIO_THRESHOLDS, l),
58
+        videoQuality: _getLevel(LOSS_VIDEO_THRESHOLDS, l)
59
+    };
60
+    const throughput = {
61
+        audioQuality: _getLevel(THROUGHPUT_AUDIO_THRESHOLDS, t, false),
62
+        videoQuality: _getLevel(THROUGHPUT_VIDEO_THRESHOLDS, t, false)
63
+    };
64
+    let connectionType = CONNECTION_TYPE.NONE;
65
+    const connectionDetails = [];
66
+
67
+    if (throughput.audioQuality === 0 || loss.audioQuality === 0) {
68
+        // Calls are impossible.
69
+        connectionType = CONNECTION_TYPE.POOR;
70
+        connectionDetails.push('prejoin.connectionDetails.veryPoorConnection');
71
+    } else if (
72
+        throughput.audioQuality === 2
73
+        && throughput.videoQuality === 2
74
+        && loss.audioQuality === 2
75
+        && loss.videoQuality === 3
76
+    ) {
77
+        // Ideal conditions for both audio and video. Show only one message.
78
+        connectionType = CONNECTION_TYPE.GOOD;
79
+        connectionDetails.push('prejoin.connectionDetails.goodQuality');
80
+    } else {
81
+        connectionType = CONNECTION_TYPE.NON_OPTIMAL;
82
+
83
+        if (throughput.audioQuality === 1) {
84
+            // Minimum requirements for a call are met.
85
+            connectionDetails.push('prejoin.connectionDetails.audioLowNoVideo');
86
+        } else {
87
+            // There are two paragraphs: one saying something about audio and the other about video.
88
+            if (loss.audioQuality === 1) {
89
+                connectionDetails.push('prejoin.connectionDetails.audioClipping');
90
+            } else {
91
+                connectionDetails.push('prejoin.connectionDetails.audioHighQuality');
92
+            }
93
+
94
+            if (throughput.videoQuality === 0 || loss.videoQuality === 0) {
95
+                connectionDetails.push('prejoin.connectionDetails.noVideo');
96
+            } else if (throughput.videoQuality === 1) {
97
+                connectionDetails.push('prejoin.connectionDetails.videoLowQuality');
98
+            } else if (loss.videoQuality === 1) {
99
+                connectionDetails.push('prejoin.connectionDetails.videoFreezing');
100
+            } else if (loss.videoQuality === 2) {
101
+                connectionDetails.push('prejoin.connectionDetails.videoTearing');
102
+            } else {
103
+                connectionDetails.push('prejoin.connectionDetails.videoHighQuality');
104
+            }
105
+        }
106
+        connectionDetails.push('prejoin.connectionDetails.undetectable');
107
+    }
108
+
109
+    return {
110
+        connectionType,
111
+        connectionDetails
112
+    };
113
+}
114
+
115
+/**
116
+ * Selector for determining the connection type & details.
117
+ *
118
+ * @param {Object} state - The state of the app.
119
+ * @returns {{
120
+ *   connectionType: string,
121
+ *   connectionDetails: string[]
122
+ * }}
123
+ */
124
+export function getConnectionData(state) {
125
+    const { precallTestResults } = state['features/prejoin'];
126
+
127
+    if (precallTestResults) {
128
+        if (precallTestResults.mediaConnectivity) {
129
+            return _getConnectionDataFromTestResults(precallTestResults);
130
+        }
131
+
132
+        return {
133
+            connectionType: CONNECTION_TYPE.POOR,
134
+            connectionDetails: [ 'prejoin.connectionDetails.noMediaConnectivity' ]
135
+        };
136
+    }
137
+
138
+    return {
139
+        connectionType: CONNECTION_TYPE.NONE,
140
+        connectionDetails: []
141
+    };
142
+}

+ 5
- 0
react/features/prejoin/actionTypes.js Näytä tiedosto

@@ -39,6 +39,11 @@ export const SET_DIALOUT_STATUS = 'SET_DIALOUT_STATUS';
39 39
  */
40 40
 export const SET_JOIN_BY_PHONE_DIALOG_VISIBLITY = 'SET_JOIN_BY_PHONE_DIALOG_VISIBLITY';
41 41
 
42
+/**
43
+ * Action type to set the precall test data.
44
+ */
45
+export const SET_PRECALL_TEST_RESULTS = 'SET_PRECALL_TEST_RESULTS';
46
+
42 47
 /**
43 48
  * Action type to disable the audio while on prejoin page.
44 49
  */

+ 32
- 0
react/features/prejoin/actions.js Näytä tiedosto

@@ -1,5 +1,7 @@
1 1
 // @flow
2 2
 
3
+declare var JitsiMeetJS: Object;
4
+
3 5
 import uuid from 'uuid';
4 6
 
5 7
 import { getRoomName } from '../base/conference';
@@ -24,6 +26,7 @@ import {
24 26
     SET_PREJOIN_DISPLAY_NAME_REQUIRED,
25 27
     SET_SKIP_PREJOIN,
26 28
     SET_JOIN_BY_PHONE_DIALOG_VISIBLITY,
29
+    SET_PRECALL_TEST_RESULTS,
27 30
     SET_PREJOIN_DEVICE_ERRORS,
28 31
     SET_PREJOIN_PAGE_VISIBILITY
29 32
 } from './actionTypes';
@@ -231,6 +234,22 @@ export function joinConferenceWithoutAudio() {
231 234
     };
232 235
 }
233 236
 
237
+/**
238
+ * Initializes the 'precallTest' and executes one test, storing the results.
239
+ *
240
+ * @param {Object} conferenceOptions - The conference options.
241
+ * @returns {Function}
242
+ */
243
+export function makePrecallTest(conferenceOptions: Object) {
244
+    return async function(dispatch: Function) {
245
+        await JitsiMeetJS.precallTest.init(conferenceOptions);
246
+
247
+        const results = await JitsiMeetJS.precallTest.execute();
248
+
249
+        dispatch(setPrecallTestResults(results));
250
+    };
251
+}
252
+
234 253
 /**
235 254
  * Opens an external page with all the dial in numbers.
236 255
  *
@@ -397,6 +416,19 @@ export function setJoinByPhoneDialogVisiblity(value: boolean) {
397 416
     };
398 417
 }
399 418
 
419
+/**
420
+ * Action used to set data from precall test.
421
+ *
422
+ * @param {Object} value - The precall test results.
423
+ * @returns {Object}
424
+ */
425
+export function setPrecallTestResults(value: Object) {
426
+    return {
427
+        type: SET_PRECALL_TEST_RESULTS,
428
+        value
429
+    };
430
+}
431
+
400 432
 /**
401 433
  * Action used to set the initial errors after creating the tracks.
402 434
  *

+ 7
- 0
react/features/prejoin/reducer.js Näytä tiedosto

@@ -6,6 +6,7 @@ import {
6 6
     SET_DIALOUT_NUMBER,
7 7
     SET_DIALOUT_STATUS,
8 8
     SET_JOIN_BY_PHONE_DIALOG_VISIBLITY,
9
+    SET_PRECALL_TEST_RESULTS,
9 10
     SET_PREJOIN_DEVICE_ERRORS,
10 11
     SET_PREJOIN_DISPLAY_NAME_REQUIRED,
11 12
     SET_PREJOIN_PAGE_VISIBILITY,
@@ -45,6 +46,12 @@ ReducerRegistry.register(
45 46
             };
46 47
         }
47 48
 
49
+        case SET_PRECALL_TEST_RESULTS:
50
+            return {
51
+                ...state,
52
+                precallTestResults: action.value
53
+            };
54
+
48 55
         case SET_PREJOIN_PAGE_VISIBILITY:
49 56
             return {
50 57
                 ...state,

Loading…
Peruuta
Tallenna