Преглед изворни кода

feat(feedback): convert to react and redux (#1833)

* feat(feedback): convert to react and redux

- For styles, remove "aui-dialog2" nesting so existing styles
  can be reused.
- Remove Feedback.js and replace with calls to redux for state
  storing and accessing.
- Add dispatching to FeedbackButton instead of relying on jquery
  clicking handling so the button can be hooked into redux.

* address feedback

* remove calling to not show feedback for recorder and filmstrip
j8
virtuacoplenny пре 8 година
родитељ
комит
ff442853a2

+ 12
- 6
conference.js Прегледај датотеку

@@ -57,6 +57,7 @@ import {
57 57
 import { getLocationContextRoot } from './react/features/base/util';
58 58
 import { statsEmitter } from './react/features/connection-indicator';
59 59
 import { showDesktopPicker } from  './react/features/desktop-picker';
60
+import { maybeOpenFeedbackDialog } from './react/features/feedback';
60 61
 import {
61 62
     mediaPermissionPromptVisibilityChanged,
62 63
     suspendDetected
@@ -2398,12 +2399,17 @@ export default {
2398 2399
     hangup(requestFeedback = false) {
2399 2400
         eventEmitter.emit(JitsiMeetConferenceEvents.BEFORE_HANGUP);
2400 2401
 
2401
-        let requestFeedbackPromise = requestFeedback
2402
-                ? APP.UI.requestFeedbackOnHangup()
2403
-                // false - because the thank you dialog shouldn't be displayed
2404
-                    .catch(() => Promise.resolve(false))
2405
-                : Promise.resolve(true);// true - because the thank you dialog
2406
-                //should be displayed
2402
+        let requestFeedbackPromise;
2403
+
2404
+        if (requestFeedback) {
2405
+            requestFeedbackPromise
2406
+                = APP.store.dispatch(maybeOpenFeedbackDialog(room))
2407
+                    // false because the thank you dialog shouldn't be displayed
2408
+                    .catch(() => Promise.resolve(false));
2409
+        } else {
2410
+            requestFeedbackPromise = Promise.resolve(true);
2411
+        }
2412
+
2407 2413
         // All promises are returning Promise.resolve to make Promise.all to
2408 2414
         // be resolved when both Promises are finished. Otherwise Promise.all
2409 2415
         // will reject on first rejected Promise and we can redirect the page

+ 50
- 74
css/modals/feedback/_feedback.scss Прегледај датотеку

@@ -45,83 +45,59 @@
45 45
     animation-timing-function: ease-in-out
46 46
 }
47 47
 
48
-.feedback.aui-dialog2{
49
-    .aui-dialog2{
50
-        &-header {
51
-            background-color: $feedbackContentBg;
52
-            border-bottom-color: transparent;
53
-            padding-top: 30px;
54
-            h2 {
55
-                color: $feedbackTextColor;
56
-                text-align: center;
57
-            }
48
+.feedback-dialog {
49
+    .details {
50
+        margin-top: 20px;
51
+        padding-left: 60px;
52
+        padding-right: 60px;
53
+
54
+        textarea {
55
+            min-height: 100px;
58 56
         }
57
+    }
58
+
59
+    .input-control {
60
+        background-color: $feedbackInputBg;
61
+        color: $feedbackInputTextColor;
62
+
63
+        &::-webkit-input-placeholder {
64
+            color: $feedbackInputPlaceholderColor;
65
+        }
66
+        &::-moz-placeholder {  /* Firefox 19+ */
67
+            color: $feedbackInputPlaceholderColor;
68
+        }
69
+        &:-ms-input-placeholder {
70
+            color: $feedbackInputPlaceholderColor;
71
+        }
72
+    }
73
+
74
+    .rating {
75
+        line-height: 1.2;
76
+        margin-top: 10px;
77
+        text-align: center;
59 78
 
60
-        &-content {
61
-            background-color: $feedbackContentBg;
62
-            text-align: center;
63
-            padding: 10px 40px 20px 40px;
64
-
65
-            .input-control {
66
-                background-color: $feedbackInputBg;
67
-                color: $feedbackInputTextColor;
68
-
69
-                &::-webkit-input-placeholder {
70
-                    color: $feedbackInputPlaceholderColor;
71
-                }
72
-                &::-moz-placeholder {  /* Firefox 19+ */
73
-                    color: $feedbackInputPlaceholderColor;
74
-                }
75
-                &:-ms-input-placeholder {
76
-                    color: $feedbackInputPlaceholderColor;
77
-                }
78
-            }
79
-
80
-            .rating {
81
-                line-height: 1.2;
82
-                text-align: center;
83
-                margin-top: 10px;
84
-
85
-                .star-label {
86
-                    height: 16px;
87
-                    font-size: 14px;
88
-                    color: $rateStarLabelColor;
89
-                }
90
-                .star-btn {
91
-                    display: inline-block;
92
-                    color: $rateStarDefault;
93
-                    font-size: $rateStarSize;
94
-                    position: relative;
95
-                    cursor: pointer;
96
-                    outline: none;
97
-                    text-decoration: none;
98
-                    @include transition(all .2s ease);
99
-
100
-                    &.starHover,
101
-                    &.active,
102
-                    &:hover {
103
-                        color: $rateStarActivity;
104
-                    };
105
-
106
-                }
107
-            }
108
-
109
-            .details {
110
-                padding-left: 60px;
111
-                padding-right: 60px;
112
-                margin-top: 20px;
113
-                textarea {
114
-                    min-height: 100px;
115
-                }
116
-            }
79
+        .star-label {
80
+            color: $rateStarLabelColor;
81
+            font-size: 14px;
82
+            height: 16px;
117 83
         }
118
-        &-footer {
119
-            background-color: $feedbackContentBg;
120
-            border-top-color: transparent;
121 84
 
122
-            .button-control {
123
-                color: $feedbackCancelFontColor;
124
-            }
85
+        .star-btn {
86
+            color: $rateStarDefault;
87
+            cursor: pointer;
88
+            display: inline-block;
89
+            font-size: $rateStarSize;
90
+            outline: none;
91
+            position: relative;
92
+            text-decoration: none;
93
+            @include transition(all .2s ease);
94
+
95
+            &.active,
96
+            &:hover,
97
+            &.starHover {
98
+                color: $rateStarActivity;
99
+            };
100
+
125 101
         }
126 102
     }
127
-}
103
+}

+ 8
- 1
lang/main.json Прегледај датотеку

@@ -301,7 +301,6 @@
301 301
         "enterDisplayName": "Please enter your display name",
302 302
         "extensionRequired": "Extension required:",
303 303
         "firefoxExtensionPrompt": "You need to install a Firefox extension in order to use screen sharing. Please try again after you <a href='__url__'>get it from here</a>!",
304
-        "rateExperience": "Please rate your meeting experience.",
305 304
         "feedbackHelp": "Your feedback will help us to improve our video experience.",
306 305
         "feedbackQuestion": "Tell us about your call!",
307 306
         "thankYou": "Thank you for using __appName__!",
@@ -478,5 +477,13 @@
478 477
     "deviceError": {
479 478
         "cameraPermission": "Error obtaining camera permission",
480 479
         "microphonePermission": "Error obtaining microphone permission"
480
+    },
481
+    "feedback": {
482
+        "average": "Average",
483
+        "bad": "Bad",
484
+        "good": "Good",
485
+        "rateExperience": "Please rate your meeting experience.",
486
+        "veryBad": "Very Bad",
487
+        "veryGood": "Very Good"
481 488
     }
482 489
 }

+ 0
- 43
modules/UI/UI.js Прегледај датотеку

@@ -21,7 +21,6 @@ import Filmstrip from "./videolayout/Filmstrip";
21 21
 import SettingsMenu from "./side_pannels/settings/SettingsMenu";
22 22
 import Profile from "./side_pannels/profile/Profile";
23 23
 import Settings from "./../settings/Settings";
24
-import { FEEDBACK_REQUEST_IN_PROGRESS } from './UIErrors';
25 24
 import { debounce } from "../util/helpers";
26 25
 
27 26
 import { updateDeviceList } from '../../react/features/base/devices';
@@ -47,7 +46,6 @@ import {
47 46
 
48 47
 var EventEmitter = require("events");
49 48
 UI.messageHandler = messageHandler;
50
-import Feedback from "./feedback/Feedback";
51 49
 import FollowMe from "../FollowMe";
52 50
 
53 51
 var eventEmitter = new EventEmitter();
@@ -228,10 +226,6 @@ UI.initConference = function () {
228 226
 
229 227
     APP.store.dispatch(checkAutoEnableDesktopSharing());
230 228
 
231
-    if(!interfaceConfig.filmStripOnly) {
232
-        Feedback.init(eventEmitter);
233
-    }
234
-
235 229
     // FollowMe attempts to copy certain aspects of the moderator's UI into the
236 230
     // other participants' UI. Consequently, it needs (1) read and write access
237 231
     // to the UI (depending on the moderator role of the local participant) and
@@ -922,43 +916,6 @@ UI.addMessage = function (from, displayName, message, stamp) {
922 916
 UI.updateDTMFSupport
923 917
     = isDTMFSupported => APP.store.dispatch(showDialPadButton(isDTMFSupported));
924 918
 
925
-/**
926
- * Show user feedback dialog if its required and enabled after pressing the
927
- * hangup button.
928
- * @returns {Promise} Resolved with value - false if the dialog is enabled and
929
- * resolved with true if the dialog is disabled or the feedback was already
930
- * submitted. Rejected if another dialog is already displayed. This values are
931
- * used to display or not display the thank you dialog from
932
- * conference.maybeRedirectToWelcomePage method.
933
- */
934
-UI.requestFeedbackOnHangup = function () {
935
-    if (Feedback.isVisible())
936
-        return Promise.reject(FEEDBACK_REQUEST_IN_PROGRESS);
937
-    // Feedback has been submitted already.
938
-    else if (Feedback.isEnabled() && Feedback.isSubmitted()) {
939
-        return Promise.resolve({
940
-            thankYouDialogVisible : true,
941
-            feedbackSubmitted: true
942
-        });
943
-    }
944
-    else
945
-        return new Promise(function (resolve) {
946
-            if (Feedback.isEnabled()) {
947
-                Feedback.openFeedbackWindow(
948
-                    (options) => {
949
-                        options.thankYouDialogVisible = false;
950
-                        resolve(options);
951
-                    });
952
-            } else {
953
-                // If the feedback functionality isn't enabled we show a thank
954
-                // you dialog. Signaling it (true), so the caller
955
-                // of requestFeedback can act on it
956
-                resolve(
957
-                    {thankYouDialogVisible : true, feedbackSubmitted: false});
958
-            }
959
-        });
960
-};
961
-
962 919
 UI.updateRecordingState = function (state) {
963 920
     Recording.updateRecordingState(state);
964 921
 };

+ 0
- 95
modules/UI/feedback/Feedback.js Прегледај датотеку

@@ -1,95 +0,0 @@
1
-/* global $, APP, JitsiMeetJS */
2
-import FeedbackWindow from "./FeedbackWindow";
3
-
4
-/**
5
- * Defines all methods in connection to the Feedback window.
6
- *
7
- * @type {{openFeedbackWindow: Function}}
8
- */
9
-const Feedback = {
10
-
11
-    /**
12
-     * Initialise the Feedback functionality.
13
-     * @param emitter the EventEmitter to associate with the Feedback.
14
-     */
15
-    init: function (emitter) {
16
-        // CallStats is the way we send feedback, so we don't have to initialise
17
-        // if callstats isn't enabled.
18
-        if (!APP.conference.isCallstatsEnabled())
19
-            return;
20
-
21
-        // If enabled property is still undefined, i.e. it hasn't been set from
22
-        // some other module already, we set it to true by default.
23
-        if (typeof this.enabled == "undefined")
24
-            this.enabled = true;
25
-
26
-        this.window = new FeedbackWindow();
27
-        this.emitter = emitter;
28
-
29
-        $("#feedbackButton").click(Feedback.openFeedbackWindow);
30
-    },
31
-    /**
32
-     * Enables/ disabled the feedback feature.
33
-     */
34
-    enableFeedback: function (enable) {
35
-        this.enabled = enable;
36
-    },
37
-
38
-    /**
39
-     * Indicates if the feedback functionality is enabled.
40
-     *
41
-     * @return true if the feedback functionality is enabled, false otherwise.
42
-     */
43
-    isEnabled: function() {
44
-        return this.enabled && APP.conference.isCallstatsEnabled();
45
-    },
46
-
47
-    /**
48
-     * Returns true if the feedback window is currently visible and false
49
-     * otherwise.
50
-     * @return {boolean} true if the feedback window is visible, false
51
-     * otherwise
52
-     */
53
-    isVisible: function() {
54
-        return $(".feedback").is(":visible");
55
-    },
56
-
57
-    /**
58
-     * Indicates if the feedback is submitted.
59
-     *
60
-     * @return {boolean} {true} to indicate if the feedback is submitted,
61
-     * {false} - otherwise
62
-     */
63
-    isSubmitted: function() {
64
-        return Feedback.window.submitted;
65
-    },
66
-
67
-    /**
68
-     * Opens the feedback window.
69
-     */
70
-    openFeedbackWindow: function (callback) {
71
-        Feedback.window.show(callback);
72
-
73
-        JitsiMeetJS.analytics.sendEvent('feedback.open');
74
-    },
75
-
76
-    /**
77
-     * Returns the feedback score.
78
-     *
79
-     * @returns {*}
80
-     */
81
-    getFeedbackScore: function() {
82
-        return Feedback.window.feedbackScore;
83
-    },
84
-
85
-    /**
86
-     * Returns the feedback free text.
87
-     *
88
-     * @returns {null|*|message}
89
-     */
90
-    getFeedbackText: function() {
91
-        return Feedback.window.feedbackText;
92
-    }
93
-};
94
-
95
-export default Feedback;

+ 0
- 184
modules/UI/feedback/FeedbackWindow.js Прегледај датотеку

@@ -1,184 +0,0 @@
1
-/* global $, APP, interfaceConfig */
2
-
3
-const labels = {
4
-    1: 'Very Bad',
5
-    2: 'Bad',
6
-    3: 'Average',
7
-    4: 'Good',
8
-    5: 'Very Good'
9
-};
10
-
11
-/**
12
- * Toggles the appropriate css class for the given number of stars, to
13
- * indicate that those stars have been clicked/selected.
14
- *
15
- * @param starCount the number of stars, for which to toggle the css class
16
- */
17
-function toggleStars(starCount) {
18
-    let labelEl = $('#starLabel');
19
-    let label = starCount >= 0 ?
20
-        labels[starCount + 1] :
21
-        '';
22
-
23
-    $('#stars > a').each(function(index, el) {
24
-        if (index <= starCount) {
25
-            el.classList.add("starHover");
26
-        } else
27
-            el.classList.remove("starHover");
28
-    });
29
-    labelEl.text(label);
30
-}
31
-
32
-/**
33
- * Constructs the html for the rated feedback window.
34
- *
35
- * @returns {string} the contructed html string
36
- */
37
-function createRateFeedbackHTML() {
38
-
39
-    let starClassName = (interfaceConfig.ENABLE_FEEDBACK_ANIMATION)
40
-        ? "icon-star-full shake-rotate"
41
-        : "icon-star-full";
42
-
43
-    return `
44
-        <form id="feedbackForm"
45
-            action="javascript:false;" onsubmit="return false;">
46
-            <div class="rating">
47
-                <div class="star-label">
48
-                    <p id="starLabel">&nbsp;</p>
49
-                </div>
50
-                <div id="stars" class="feedback-stars">
51
-                    <a class="star-btn">
52
-                        <i class=${ starClassName }></i>
53
-                    </a>
54
-                    <a class="star-btn">
55
-                        <i class=${ starClassName }></i>
56
-                    </a>
57
-                    <a class="star-btn">
58
-                        <i class=${ starClassName }></i>
59
-                    </a>
60
-                    <a class="star-btn">
61
-                        <i class=${ starClassName }></i>
62
-                    </a>
63
-                    <a class="star-btn">
64
-                        <i class=${ starClassName }></i>
65
-                    </a>
66
-                </div>
67
-            </div>
68
-            <div class="details">
69
-                <textarea id="feedbackTextArea" class="input-control" 
70
-                    data-i18n="[placeholder]dialog.feedbackHelp"></textarea>
71
-            </div>
72
-        </form>`;
73
-}
74
-
75
-/**
76
- * Feedback is loaded callback
77
- * Calls when Modal window is in DOM
78
- *
79
- * @param Feedback
80
- */
81
-let onLoadFunction = function (Feedback) {
82
-    $('#stars > a').each((index, el) => {
83
-        el.onmouseover = function(){
84
-            toggleStars(index);
85
-        };
86
-        el.onmouseleave = function(){
87
-            toggleStars(Feedback.feedbackScore - 1);
88
-        };
89
-        el.onclick = function(){
90
-            Feedback.feedbackScore = index + 1;
91
-            Feedback.setFeedbackMessage();
92
-        };
93
-    });
94
-
95
-    // Init stars to correspond to previously entered feedback.
96
-    if (Feedback.feedbackScore > 0) {
97
-        toggleStars(Feedback.feedbackScore - 1);
98
-    }
99
-
100
-    if (Feedback.feedbackMessage && Feedback.feedbackMessage.length > 0)
101
-        $('#feedbackTextArea').text(Feedback.feedbackMessage);
102
-
103
-    $('#feedbackTextArea').focus();
104
-};
105
-
106
-/**
107
- * On Feedback Submitted callback
108
- *
109
- * @param Feedback
110
- */
111
-function onFeedbackSubmitted(Feedback) {
112
-    let form = $('#feedbackForm');
113
-    let message = form.find('textarea').val();
114
-
115
-    APP.conference.sendFeedback(
116
-        Feedback.feedbackScore,
117
-        message);
118
-
119
-    // TODO: make sendFeedback return true or false.
120
-    Feedback.submitted = true;
121
-
122
-    //Remove history is submitted
123
-    Feedback.feedbackScore = -1;
124
-    Feedback.feedbackMessage = '';
125
-    Feedback.onHide();
126
-}
127
-
128
-/**
129
- * On Feedback Closed callback
130
- *
131
- * @param Feedback
132
- */
133
-function onFeedbackClosed(Feedback) {
134
-    Feedback.onHide();
135
-}
136
-
137
-/**
138
- * @class Dialog
139
- *
140
- */
141
-export default class Dialog {
142
-
143
-    constructor() {
144
-        this.feedbackScore = -1;
145
-        this.feedbackMessage = '';
146
-        this.submitted = false;
147
-        this.onCloseCallback = function() {};
148
-
149
-        this.setDefaultOptions();
150
-    }
151
-
152
-    setDefaultOptions() {
153
-        var self = this;
154
-
155
-        this.options = {
156
-            titleKey: 'dialog.rateExperience',
157
-            msgString: createRateFeedbackHTML(),
158
-            loadedFunction: function() {onLoadFunction(self);},
159
-            submitFunction: function() {onFeedbackSubmitted(self);},
160
-            closeFunction: function() {onFeedbackClosed(self);},
161
-            wrapperClass: 'feedback',
162
-            size: 'medium'
163
-        };
164
-    }
165
-
166
-    setFeedbackMessage() {
167
-        this.feedbackMessage = $('#feedbackTextArea').val();
168
-    }
169
-
170
-    show(cb) {
171
-        const options = this.options;
172
-        if (typeof cb === 'function') {
173
-            this.onCloseCallback = cb;
174
-        }
175
-
176
-        this.window = APP.UI.messageHandler.openTwoButtonDialog(options);
177
-    }
178
-
179
-    onHide() {
180
-        this.onCloseCallback({
181
-            feedbackSubmitted: this.submitted
182
-        });
183
-    }
184
-}

+ 0
- 2
modules/UI/recording/Recording.js Прегледај датотеку

@@ -19,7 +19,6 @@ const logger = require("jitsi-meet-logger").getLogger(__filename);
19 19
 import UIEvents from "../../../service/UI/UIEvents";
20 20
 import UIUtil from '../util/UIUtil';
21 21
 import VideoLayout from '../videolayout/VideoLayout';
22
-import Feedback from '../feedback/Feedback.js';
23 22
 
24 23
 import { setToolboxEnabled } from '../../../react/features/toolbox';
25 24
 import { setNotificationsEnabled } from '../../../react/features/notifications';
@@ -308,7 +307,6 @@ var Recording = {
308 307
             VideoLayout.enableDeviceAvailabilityIcons(
309 308
                 APP.conference.getMyUserId(), false);
310 309
             VideoLayout.setLocalVideoVisible(false);
311
-            Feedback.enableFeedback(false);
312 310
             APP.store.dispatch(setToolboxEnabled(false));
313 311
             APP.store.dispatch(setNotificationsEnabled(false));
314 312
             APP.UI.messageHandler.enablePopups(false);

+ 21
- 0
react/features/feedback/actionTypes.js Прегледај датотеку

@@ -0,0 +1,21 @@
1
+/**
2
+ * The type of the action which signals feedback was closed without submitting.
3
+ *
4
+ * {
5
+ *     type: CANCEL_FEEDBACK,
6
+ *     message: string,
7
+ *     score: number
8
+ * }
9
+ */
10
+export const CANCEL_FEEDBACK = Symbol('CANCEL_FEEDBACK');
11
+
12
+/**
13
+ * The type of the action which signals feedback was submitted for recording.
14
+ *
15
+ * {
16
+ *     type: SUBMIT_FEEDBACK,
17
+ *     message: string,
18
+ *     score: number
19
+ * }
20
+ */
21
+export const SUBMIT_FEEDBACK = Symbol('SUBMIT_FEEDBACK');

+ 122
- 0
react/features/feedback/actions.js Прегледај датотеку

@@ -0,0 +1,122 @@
1
+import { FEEDBACK_REQUEST_IN_PROGRESS } from '../../../modules/UI/UIErrors';
2
+
3
+import { openDialog } from '../../features/base/dialog';
4
+
5
+import {
6
+    CANCEL_FEEDBACK,
7
+    SUBMIT_FEEDBACK
8
+} from './actionTypes';
9
+import { FeedbackDialog } from './components';
10
+
11
+declare var config: Object;
12
+declare var interfaceConfig: Object;
13
+
14
+/**
15
+ * Caches the passed in feedback in the redux store.
16
+ *
17
+ * @param {number} score - The quality score given to the conference.
18
+ * @param {string} message - A description entered by the participant that
19
+ * explains the rating.
20
+ * @returns {{
21
+ *     type: CANCEL_FEEDBACK,
22
+ *     message: string,
23
+ *     score: number
24
+ * }}
25
+ */
26
+export function cancelFeedback(score, message) {
27
+    return {
28
+        type: CANCEL_FEEDBACK,
29
+        message,
30
+        score
31
+    };
32
+}
33
+
34
+/**
35
+ * Potentially open the {@code FeedbackDialog}. It will not be opened if it is
36
+ * already open or feedback has already been submitted.
37
+ *
38
+ * @param {JistiConference} conference - The conference for which the feedback
39
+ * would be about. The conference is passed in because feedback can occur after
40
+ * a conference has been left, so references to it may no longer exist in redux.
41
+ * @returns {Promise} Resolved with value - false if the dialog is enabled and
42
+ * resolved with true if the dialog is disabled or the feedback was already
43
+ * submitted. Rejected if another dialog is already displayed.
44
+ */
45
+export function maybeOpenFeedbackDialog(conference) {
46
+    return (dispatch, getState) => {
47
+        const state = getState();
48
+
49
+        if (interfaceConfig.filmStripOnly || config.iAmRecorder) {
50
+            // Intentionally fall through the if chain to prevent further action
51
+            // from being taken with regards to showing feedback.
52
+        } else if (state['features/base/dialog'].component === FeedbackDialog) {
53
+            // Feedback is currently being displayed.
54
+
55
+            return Promise.reject(FEEDBACK_REQUEST_IN_PROGRESS);
56
+        } else if (state['features/feedback'].submitted) {
57
+            // Feedback has been submitted already.
58
+
59
+            return Promise.resolve({
60
+                thankYouDialogVisible: true,
61
+                feedbackSubmitted: true
62
+            });
63
+        } else if (conference.isCallstatsEnabled()) {
64
+            return new Promise(resolve => {
65
+                dispatch(openFeedbackDialog(conference, () => {
66
+                    const { submitted } = getState()['features/feedback'];
67
+
68
+                    resolve({
69
+                        feedbackSubmitted: submitted,
70
+                        thankYouDialogVisible: false
71
+                    });
72
+                }));
73
+            });
74
+        }
75
+
76
+        // If the feedback functionality isn't enabled we show a thank
77
+        // you dialog. Signaling it (true), so the caller
78
+        // of requestFeedback can act on it
79
+        return Promise.resolve({
80
+            thankYouDialogVisible: true,
81
+            feedbackSubmitted: false
82
+        });
83
+    };
84
+}
85
+
86
+/**
87
+ * Opens {@code FeedbackDialog}.
88
+ *
89
+ * @param {JitsiConference} conference - The JitsiConference that is being
90
+ * rated. The conference is passed in because feedback can occur after a
91
+ * conference has been left, so references to it may no longer exist in redux.
92
+ * @param {Function} [onClose] - An optional callback to invoke when the dialog
93
+ * is closed.
94
+ * @returns {Object}
95
+ */
96
+export function openFeedbackDialog(conference, onClose) {
97
+    return openDialog(FeedbackDialog, {
98
+        conference,
99
+        onClose
100
+    });
101
+}
102
+
103
+/**
104
+ * Send the passed in feedback.
105
+ *
106
+ * @param {number} score - An integer between 1 and 5 indicating the user
107
+ * feedback. The negative integer -1 is used to denote no score was selected.
108
+ * @param {string} message - Detailed feedback from the user to explain the
109
+ * rating.
110
+ * @param {JitsiConference} conference - The JitsiConference for which the
111
+ * feedback is being left.
112
+ * @returns {{
113
+ *     type: SUBMIT_FEEDBACK
114
+ * }}
115
+ */
116
+export function submitFeedback(score, message, conference) {
117
+    conference.sendFeedback(score, message);
118
+
119
+    return {
120
+        type: SUBMIT_FEEDBACK
121
+    };
122
+}

+ 51
- 13
react/features/feedback/components/FeedbackButton.web.js Прегледај датотеку

@@ -1,15 +1,23 @@
1 1
 /* @flow */
2 2
 
3 3
 import React, { Component } from 'react';
4
+import { connect } from 'react-redux';
4 5
 
5
-declare var config: Object;
6
+import { openFeedbackDialog } from '../actions';
6 7
 
7 8
 /**
8 9
  * Implements a Web/React Component which renders a feedback button.
9 10
  */
10
-export class FeedbackButton extends Component {
11
-    state = {
12
-        callStatsID: String
11
+class FeedbackButton extends Component {
12
+    _onClick: Function;
13
+
14
+    static propTypes = {
15
+        /**
16
+         * The JitsiConference for which the feedback will be about.
17
+         *
18
+         * @type {JitsiConference}
19
+         */
20
+        _conference: React.PropTypes.object
13 21
     };
14 22
 
15 23
     /**
@@ -21,9 +29,8 @@ export class FeedbackButton extends Component {
21 29
     constructor(props: Object) {
22 30
         super(props);
23 31
 
24
-        this.state = {
25
-            callStatsID: config.callStatsID
26
-        };
32
+        // Bind event handlers so they are only bound once for every instance.
33
+        this._onClick = this._onClick.bind(this);
27 34
     }
28 35
 
29 36
     /**
@@ -33,15 +40,46 @@ export class FeedbackButton extends Component {
33 40
      * @returns {ReactElement}
34 41
      */
35 42
     render() {
36
-        // If callstats.io-support is not configured, skip rendering.
37
-        if (!this.state.callStatsID) {
38
-            return null;
39
-        }
40
-
41 43
         return (
42 44
             <a
43 45
                 className = 'button icon-feedback'
44
-                id = 'feedbackButton' />
46
+                id = 'feedbackButton'
47
+                onClick = { this._onClick } />
45 48
         );
46 49
     }
50
+
51
+    /**
52
+     * Dispatches an action to open a dialog requesting call feedback.
53
+     *
54
+     * @private
55
+     * @returns {void}
56
+     */
57
+    _onClick() {
58
+        const { _conference, dispatch } = this.props;
59
+
60
+        dispatch(openFeedbackDialog(_conference));
61
+    }
47 62
 }
63
+
64
+/**
65
+ * Maps (parts of) the Redux state to the associated Conference's props.
66
+ *
67
+ * @param {Object} state - The Redux state.
68
+ * @private
69
+ * @returns {{
70
+ *     _toolboxVisible: boolean
71
+ * }}
72
+ */
73
+function _mapStateToProps(state) {
74
+    return {
75
+        /**
76
+         * The JitsiConference for which the feedback will be about.
77
+         *
78
+         * @private
79
+         * @type {JitsiConference}
80
+         */
81
+        _conference: state['features/base/conference'].conference
82
+    };
83
+}
84
+
85
+export default connect(_mapStateToProps)(FeedbackButton);

+ 343
- 0
react/features/feedback/components/FeedbackDialog.web.js Прегледај датотеку

@@ -0,0 +1,343 @@
1
+import StarIcon from '@atlaskit/icon/glyph/star';
2
+import StarFilledIcon from '@atlaskit/icon/glyph/star-filled';
3
+import React, { Component } from 'react';
4
+import { connect } from 'react-redux';
5
+
6
+import { Dialog } from '../../base/dialog';
7
+import { translate } from '../../base/i18n';
8
+import JitsiMeetJS from '../../base/lib-jitsi-meet';
9
+
10
+import { cancelFeedback, submitFeedback } from '../actions';
11
+
12
+declare var interfaceConfig: Object;
13
+
14
+const scoreAnimationClass = interfaceConfig.ENABLE_FEEDBACK_ANIMATION
15
+    ? 'shake-rotate' : '';
16
+
17
+/**
18
+ * The scores to display for selecting. The score is the index in the array and
19
+ * the value of the index is a translation key used for display in the dialog.
20
+ *
21
+ * @types {string[]}
22
+ */
23
+const SCORES = [
24
+    'feedback.veryBad',
25
+    'feedback.bad',
26
+    'feedback.average',
27
+    'feedback.good',
28
+    'feedback.veryGood'
29
+];
30
+
31
+/**
32
+ * A React {@code Component} for displaying a dialog to rate the current
33
+ * conference quality, write a message describing the experience, and submit
34
+ * the feedback.
35
+ *
36
+ * @extends Component
37
+ */
38
+class FeedbackDialog extends Component {
39
+    /**
40
+     * {@code FeedbackDialog} component's property types.
41
+     *
42
+     * @static
43
+     */
44
+    static propTypes = {
45
+        /**
46
+         * The cached feedback message, if any, that was set when closing a
47
+         * previous instance of {@code FeedbackDialog}.
48
+         */
49
+        _message: React.PropTypes.string,
50
+
51
+        /**
52
+         * The cached feedback score, if any, that was set when closing a
53
+         * previous instance of {@code FeedbackDialog}.
54
+         */
55
+        _score: React.PropTypes.number,
56
+
57
+        /**
58
+         * The JitsiConference that is being rated. The conference is passed in
59
+         * because feedback can occur after a conference has been left, so
60
+         * references to it may no longer exist in redux.
61
+         *
62
+         * @type {JitsiConference}
63
+         */
64
+        conference: React.PropTypes.object,
65
+
66
+        /**
67
+         * Invoked to signal feedback submission or canceling.
68
+         */
69
+        dispatch: React.PropTypes.func,
70
+
71
+        /**
72
+         * Callback invoked when {@code FeedbackDialog} is unmounted.
73
+         */
74
+        onClose: React.PropTypes.func,
75
+
76
+        /**
77
+         * Invoked to obtain translated strings.
78
+         */
79
+        t: React.PropTypes.func
80
+    };
81
+
82
+    /**
83
+     * Initializes a new {@code FeedbackDialog} instance.
84
+     *
85
+     * @param {Object} props - The read-only React {@code Component} props with
86
+     * which the new instance is to be initialized.
87
+     */
88
+    constructor(props) {
89
+        super(props);
90
+
91
+        const { _message, _score } = this.props;
92
+
93
+        this.state = {
94
+            /**
95
+             * The currently entered feedback message.
96
+             *
97
+             * @type {string}
98
+             */
99
+            message: _message,
100
+
101
+            /**
102
+             * The score selection index which is currently being hovered. The
103
+             * value -1 is used as a sentinel value to match store behavior of
104
+             * using -1 for no score having been selected.
105
+             *
106
+             * @type {number}
107
+             */
108
+            mousedOverScore: -1,
109
+
110
+            /**
111
+             * The currently selected score selection index. The score will not
112
+             * be 0 indexed so subtract one to map with SCORES.
113
+             *
114
+             * @type {number}
115
+             */
116
+            score: _score > -1 ? _score - 1 : _score
117
+        };
118
+
119
+        /**
120
+         * An array of objects with click handlers for each of the scores listed
121
+         * in SCORES. This pattern is used for binding event handlers only once
122
+         * for each score selection icon.
123
+         *
124
+         * @type {Object[]}
125
+         */
126
+        this._scoreClickConfigurations = SCORES.map((textKey, index) => {
127
+            return {
128
+                _onClick: () => this._onScoreSelect(index),
129
+                _onMouseOver: () => this._onScoreMouseOver(index)
130
+            };
131
+        });
132
+
133
+        // Bind event handlers so they are only bound once for every instance.
134
+        this._onCancel = this._onCancel.bind(this);
135
+        this._onMessageChange = this._onMessageChange.bind(this);
136
+        this._onScoreContainerMouseLeave
137
+            = this._onScoreContainerMouseLeave.bind(this);
138
+        this._onSubmit = this._onSubmit.bind(this);
139
+    }
140
+
141
+    /**
142
+     * Emits an analytics event to notify feedback has been opened.
143
+     *
144
+     * @inheritdoc
145
+     */
146
+    componentDidMount() {
147
+        JitsiMeetJS.analytics.sendEvent('feedback.open');
148
+    }
149
+
150
+    /**
151
+     * Invokes the onClose callback, if defined, to notify of the close event.
152
+     *
153
+     * @inheritdoc
154
+     */
155
+    componentWillUnmount() {
156
+        if (this.props.onClose) {
157
+            this.props.onClose();
158
+        }
159
+    }
160
+
161
+    /**
162
+     * Implements React's {@link Component#render()}.
163
+     *
164
+     * @inheritdoc
165
+     * @returns {ReactElement}
166
+     */
167
+    render() {
168
+        const { message, mousedOverScore, score } = this.state;
169
+        const scoreToDisplayAsSelected
170
+            = mousedOverScore > -1 ? mousedOverScore : score;
171
+
172
+        const scoreIcons = this._scoreClickConfigurations.map(
173
+            (config, index) => {
174
+                const isFilled = index <= scoreToDisplayAsSelected;
175
+                const activeClass = isFilled ? 'active' : '';
176
+                const className
177
+                    = `star-btn ${scoreAnimationClass} ${activeClass}`;
178
+
179
+                return (
180
+                    <a
181
+                        className = { className }
182
+                        key = { index }
183
+                        onClick = { config._onClick }
184
+                        onMouseOver = { config._onMouseOver }>
185
+                        { isFilled
186
+                            ? <StarFilledIcon
187
+                                label = 'star-filled'
188
+                                size = 'xlarge' />
189
+                            : <StarIcon
190
+                                label = 'star'
191
+                                size = 'xlarge' /> }
192
+                    </a>
193
+                );
194
+            });
195
+
196
+        const { t } = this.props;
197
+
198
+        return (
199
+            <Dialog
200
+                okTitleKey = 'dialog.Submit'
201
+                onCancel = { this._onCancel }
202
+                onSubmit = { this._onSubmit }
203
+                titleKey = 'feedback.rateExperience'>
204
+                <div className = 'feedback-dialog'>
205
+                    <div className = 'rating'>
206
+                        <div className = 'star-label'>
207
+                            <p id = 'starLabel'>
208
+                                { t(SCORES[scoreToDisplayAsSelected]) }
209
+                            </p>
210
+                        </div>
211
+                        <div
212
+                            className = 'stars'
213
+                            onMouseLeave = { this._onScoreContainerMouseLeave }>
214
+                            { scoreIcons }
215
+                        </div>
216
+                    </div>
217
+                    <div className = 'details'>
218
+                        <textarea
219
+                            autoFocus = { true }
220
+                            className = 'input-control'
221
+                            id = 'feedbackTextArea'
222
+                            onChange = { this._onMessageChange }
223
+                            placeholder = { t('dialog.feedbackHelp') }
224
+                            value = { message } />
225
+                    </div>
226
+                </div>
227
+            </Dialog>
228
+        );
229
+    }
230
+
231
+    /**
232
+     * Dispatches an action notifying feedback was not submitted. The submitted
233
+     * score will have one added as the rest of the app does not expect 0
234
+     * indexing.
235
+     *
236
+     * @private
237
+     * @returns {boolean} Returns true to close the dialog.
238
+     */
239
+    _onCancel() {
240
+        const { message, score } = this.state;
241
+        const scoreToSubmit = score > -1 ? score + 1 : score;
242
+
243
+        this.props.dispatch(cancelFeedback(scoreToSubmit, message));
244
+
245
+        return true;
246
+    }
247
+
248
+    /**
249
+     * Updates the known entered feedback message.
250
+     *
251
+     * @param {Object} event - The DOM event from updating the textfield for the
252
+     * feedback message.
253
+     * @private
254
+     * @returns {void}
255
+     */
256
+    _onMessageChange(event) {
257
+        this.setState({ message: event.target.value });
258
+    }
259
+
260
+    /**
261
+     * Updates the currently selected score.
262
+     *
263
+     * @param {number} score - The index of the selected score in SCORES.
264
+     * @private
265
+     * @returns {void}
266
+     */
267
+    _onScoreSelect(score) {
268
+        this.setState({ score });
269
+    }
270
+
271
+    /**
272
+     * Sets the currently hovered score to null to indicate no hover is
273
+     * occurring.
274
+     *
275
+     * @private
276
+     * @returns {void}
277
+     */
278
+    _onScoreContainerMouseLeave() {
279
+        this.setState({ mousedOverScore: -1 });
280
+    }
281
+
282
+    /**
283
+     * Updates the known state of the score icon currently behind hovered over.
284
+     *
285
+     * @param {number} mousedOverScore - The index of the SCORES value currently
286
+     * being moused over.
287
+     * @private
288
+     * @returns {void}
289
+     */
290
+    _onScoreMouseOver(mousedOverScore) {
291
+        this.setState({ mousedOverScore });
292
+    }
293
+
294
+    /**
295
+     * Dispatches the entered feedback for submission. The submitted score will
296
+     * have one added as the rest of the app does not expect 0 indexing.
297
+     *
298
+     * @private
299
+     * @returns {boolean} Returns true to close the dialog.
300
+     */
301
+    _onSubmit() {
302
+        const { conference, dispatch } = this.props;
303
+        const { message, score } = this.state;
304
+
305
+        const scoreToSubmit = score > -1 ? score + 1 : score;
306
+
307
+        dispatch(submitFeedback(scoreToSubmit, message, conference));
308
+
309
+        return true;
310
+    }
311
+}
312
+
313
+/**
314
+ * Maps (parts of) the Redux state to the associated {@code FeedbackDialog}'s
315
+ * props.
316
+ *
317
+ * @param {Object} state - The Redux state.
318
+ * @private
319
+ * @returns {{
320
+ * }}
321
+ */
322
+function _mapStateToProps(state) {
323
+    const { message, score } = state['features/feedback'];
324
+
325
+    return {
326
+        /**
327
+         * The cached feedback message, if any, that was set when closing a
328
+         * previous instance of {@code FeedbackDialog}.
329
+         *
330
+         * @type {string}
331
+         */
332
+        _message: message,
333
+
334
+        /**
335
+         * The currently selected score selection index.
336
+         *
337
+         * @type {number}
338
+         */
339
+        _score: score
340
+    };
341
+}
342
+
343
+export default translate(connect(_mapStateToProps)(FeedbackDialog));

+ 2
- 1
react/features/feedback/components/index.js Прегледај датотеку

@@ -1 +1,2 @@
1
-export * from './FeedbackButton';
1
+export { default as FeedbackButton } from './FeedbackButton';
2
+export { default as FeedbackDialog } from './FeedbackDialog';

+ 4
- 0
react/features/feedback/index.js Прегледај датотеку

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

+ 45
- 0
react/features/feedback/reducer.js Прегледај датотеку

@@ -0,0 +1,45 @@
1
+import {
2
+    ReducerRegistry
3
+} from '../base/redux';
4
+
5
+import {
6
+    CANCEL_FEEDBACK,
7
+    SUBMIT_FEEDBACK
8
+} from './actionTypes';
9
+
10
+const DEFAULT_STATE = {
11
+    message: '',
12
+
13
+    // The sentinel value -1 is used to denote no rating has been set and to
14
+    // preserve pre-redux behavior.
15
+    score: -1,
16
+    submitted: false
17
+};
18
+
19
+/**
20
+ * Reduces the Redux actions of the feature features/feedback.
21
+ */
22
+ReducerRegistry.register(
23
+    'features/feedback',
24
+    (state = DEFAULT_STATE, action) => {
25
+        switch (action.type) {
26
+        case CANCEL_FEEDBACK: {
27
+            return {
28
+                ...state,
29
+                message: action.message,
30
+                score: action.score
31
+            };
32
+        }
33
+
34
+        case SUBMIT_FEEDBACK: {
35
+            return {
36
+                ...state,
37
+                message: '',
38
+                score: -1,
39
+                submitted: true
40
+            };
41
+        }
42
+        }
43
+
44
+        return state;
45
+    });

+ 15
- 2
react/features/toolbox/components/SecondaryToolbar.web.js Прегледај датотеку

@@ -13,6 +13,7 @@ import { getToolbarClassNames } from '../functions';
13 13
 import Toolbar from './Toolbar';
14 14
 
15 15
 declare var APP: Object;
16
+declare var config: Object;
16 17
 
17 18
 /**
18 19
  * Implementation of secondary toolbar React component.
@@ -29,6 +30,12 @@ class SecondaryToolbar extends Component {
29 30
      * @static
30 31
      */
31 32
     static propTypes = {
33
+        /**
34
+         * Application ID for callstats.io API. The {@code FeedbackButton} will
35
+         * display if defined.
36
+         */
37
+        _callStatsID: React.PropTypes.string,
38
+
32 39
         /**
33 40
          * The indicator which determines whether the local participant is a
34 41
          * guest in the conference.
@@ -79,7 +86,7 @@ class SecondaryToolbar extends Component {
79 86
      * @returns {ReactElement}
80 87
      */
81 88
     render(): ReactElement<*> | null {
82
-        const { _secondaryToolbarButtons } = this.props;
89
+        const { _callStatsID, _secondaryToolbarButtons } = this.props;
83 90
 
84 91
         // The number of buttons to show in the toolbar isn't fixed, it depends
85 92
         // on the availability of features and configuration parameters. So
@@ -95,7 +102,7 @@ class SecondaryToolbar extends Component {
95 102
                 className = { secondaryToolbarClassName }
96 103
                 toolbarButtons = { _secondaryToolbarButtons }
97 104
                 tooltipPosition = { 'right' }>
98
-                <FeedbackButton />
105
+                { _callStatsID ? <FeedbackButton /> : null }
99 106
             </Toolbar>
100 107
         );
101 108
     }
@@ -140,8 +147,14 @@ function _mapDispatchToProps(dispatch: Function): Object {
140 147
 function _mapStateToProps(state: Object): Object {
141 148
     const { isGuest } = state['features/jwt'];
142 149
     const { secondaryToolbarButtons, visible } = state['features/toolbox'];
150
+    const { callStatsID } = state['features/base/config'];
143 151
 
144 152
     return {
153
+        /**
154
+         * Application ID for callstats.io API.
155
+         */
156
+        _callStatsID: callStatsID,
157
+
145 158
         /**
146 159
          * The indicator which determines whether the local participant is a
147 160
          * guest in the conference.

Loading…
Откажи
Сачувај