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
This commit is contained in:
parent
85a168d51b
commit
ff442853a2
|
@ -57,6 +57,7 @@ import {
|
|||
import { getLocationContextRoot } from './react/features/base/util';
|
||||
import { statsEmitter } from './react/features/connection-indicator';
|
||||
import { showDesktopPicker } from './react/features/desktop-picker';
|
||||
import { maybeOpenFeedbackDialog } from './react/features/feedback';
|
||||
import {
|
||||
mediaPermissionPromptVisibilityChanged,
|
||||
suspendDetected
|
||||
|
@ -2398,12 +2399,17 @@ export default {
|
|||
hangup(requestFeedback = false) {
|
||||
eventEmitter.emit(JitsiMeetConferenceEvents.BEFORE_HANGUP);
|
||||
|
||||
let requestFeedbackPromise = requestFeedback
|
||||
? APP.UI.requestFeedbackOnHangup()
|
||||
// false - because the thank you dialog shouldn't be displayed
|
||||
.catch(() => Promise.resolve(false))
|
||||
: Promise.resolve(true);// true - because the thank you dialog
|
||||
//should be displayed
|
||||
let requestFeedbackPromise;
|
||||
|
||||
if (requestFeedback) {
|
||||
requestFeedbackPromise
|
||||
= APP.store.dispatch(maybeOpenFeedbackDialog(room))
|
||||
// false because the thank you dialog shouldn't be displayed
|
||||
.catch(() => Promise.resolve(false));
|
||||
} else {
|
||||
requestFeedbackPromise = Promise.resolve(true);
|
||||
}
|
||||
|
||||
// All promises are returning Promise.resolve to make Promise.all to
|
||||
// be resolved when both Promises are finished. Otherwise Promise.all
|
||||
// will reject on first rejected Promise and we can redirect the page
|
||||
|
|
|
@ -45,83 +45,59 @@
|
|||
animation-timing-function: ease-in-out
|
||||
}
|
||||
|
||||
.feedback.aui-dialog2{
|
||||
.aui-dialog2{
|
||||
&-header {
|
||||
background-color: $feedbackContentBg;
|
||||
border-bottom-color: transparent;
|
||||
padding-top: 30px;
|
||||
h2 {
|
||||
color: $feedbackTextColor;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
.feedback-dialog {
|
||||
.details {
|
||||
margin-top: 20px;
|
||||
padding-left: 60px;
|
||||
padding-right: 60px;
|
||||
|
||||
&-content {
|
||||
background-color: $feedbackContentBg;
|
||||
text-align: center;
|
||||
padding: 10px 40px 20px 40px;
|
||||
|
||||
.input-control {
|
||||
background-color: $feedbackInputBg;
|
||||
color: $feedbackInputTextColor;
|
||||
|
||||
&::-webkit-input-placeholder {
|
||||
color: $feedbackInputPlaceholderColor;
|
||||
}
|
||||
&::-moz-placeholder { /* Firefox 19+ */
|
||||
color: $feedbackInputPlaceholderColor;
|
||||
}
|
||||
&:-ms-input-placeholder {
|
||||
color: $feedbackInputPlaceholderColor;
|
||||
}
|
||||
}
|
||||
|
||||
.rating {
|
||||
line-height: 1.2;
|
||||
text-align: center;
|
||||
margin-top: 10px;
|
||||
|
||||
.star-label {
|
||||
height: 16px;
|
||||
font-size: 14px;
|
||||
color: $rateStarLabelColor;
|
||||
}
|
||||
.star-btn {
|
||||
display: inline-block;
|
||||
color: $rateStarDefault;
|
||||
font-size: $rateStarSize;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
@include transition(all .2s ease);
|
||||
|
||||
&.starHover,
|
||||
&.active,
|
||||
&:hover {
|
||||
color: $rateStarActivity;
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.details {
|
||||
padding-left: 60px;
|
||||
padding-right: 60px;
|
||||
margin-top: 20px;
|
||||
textarea {
|
||||
min-height: 100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
&-footer {
|
||||
background-color: $feedbackContentBg;
|
||||
border-top-color: transparent;
|
||||
|
||||
.button-control {
|
||||
color: $feedbackCancelFontColor;
|
||||
}
|
||||
textarea {
|
||||
min-height: 100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input-control {
|
||||
background-color: $feedbackInputBg;
|
||||
color: $feedbackInputTextColor;
|
||||
|
||||
&::-webkit-input-placeholder {
|
||||
color: $feedbackInputPlaceholderColor;
|
||||
}
|
||||
&::-moz-placeholder { /* Firefox 19+ */
|
||||
color: $feedbackInputPlaceholderColor;
|
||||
}
|
||||
&:-ms-input-placeholder {
|
||||
color: $feedbackInputPlaceholderColor;
|
||||
}
|
||||
}
|
||||
|
||||
.rating {
|
||||
line-height: 1.2;
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
|
||||
.star-label {
|
||||
color: $rateStarLabelColor;
|
||||
font-size: 14px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.star-btn {
|
||||
color: $rateStarDefault;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-size: $rateStarSize;
|
||||
outline: none;
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
@include transition(all .2s ease);
|
||||
|
||||
&.active,
|
||||
&:hover,
|
||||
&.starHover {
|
||||
color: $rateStarActivity;
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -301,7 +301,6 @@
|
|||
"enterDisplayName": "Please enter your display name",
|
||||
"extensionRequired": "Extension required:",
|
||||
"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>!",
|
||||
"rateExperience": "Please rate your meeting experience.",
|
||||
"feedbackHelp": "Your feedback will help us to improve our video experience.",
|
||||
"feedbackQuestion": "Tell us about your call!",
|
||||
"thankYou": "Thank you for using __appName__!",
|
||||
|
@ -478,5 +477,13 @@
|
|||
"deviceError": {
|
||||
"cameraPermission": "Error obtaining camera permission",
|
||||
"microphonePermission": "Error obtaining microphone permission"
|
||||
},
|
||||
"feedback": {
|
||||
"average": "Average",
|
||||
"bad": "Bad",
|
||||
"good": "Good",
|
||||
"rateExperience": "Please rate your meeting experience.",
|
||||
"veryBad": "Very Bad",
|
||||
"veryGood": "Very Good"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,6 @@ import Filmstrip from "./videolayout/Filmstrip";
|
|||
import SettingsMenu from "./side_pannels/settings/SettingsMenu";
|
||||
import Profile from "./side_pannels/profile/Profile";
|
||||
import Settings from "./../settings/Settings";
|
||||
import { FEEDBACK_REQUEST_IN_PROGRESS } from './UIErrors';
|
||||
import { debounce } from "../util/helpers";
|
||||
|
||||
import { updateDeviceList } from '../../react/features/base/devices';
|
||||
|
@ -47,7 +46,6 @@ import {
|
|||
|
||||
var EventEmitter = require("events");
|
||||
UI.messageHandler = messageHandler;
|
||||
import Feedback from "./feedback/Feedback";
|
||||
import FollowMe from "../FollowMe";
|
||||
|
||||
var eventEmitter = new EventEmitter();
|
||||
|
@ -228,10 +226,6 @@ UI.initConference = function () {
|
|||
|
||||
APP.store.dispatch(checkAutoEnableDesktopSharing());
|
||||
|
||||
if(!interfaceConfig.filmStripOnly) {
|
||||
Feedback.init(eventEmitter);
|
||||
}
|
||||
|
||||
// FollowMe attempts to copy certain aspects of the moderator's UI into the
|
||||
// other participants' UI. Consequently, it needs (1) read and write access
|
||||
// to the UI (depending on the moderator role of the local participant) and
|
||||
|
@ -922,43 +916,6 @@ UI.addMessage = function (from, displayName, message, stamp) {
|
|||
UI.updateDTMFSupport
|
||||
= isDTMFSupported => APP.store.dispatch(showDialPadButton(isDTMFSupported));
|
||||
|
||||
/**
|
||||
* Show user feedback dialog if its required and enabled after pressing the
|
||||
* hangup button.
|
||||
* @returns {Promise} Resolved with value - false if the dialog is enabled and
|
||||
* resolved with true if the dialog is disabled or the feedback was already
|
||||
* submitted. Rejected if another dialog is already displayed. This values are
|
||||
* used to display or not display the thank you dialog from
|
||||
* conference.maybeRedirectToWelcomePage method.
|
||||
*/
|
||||
UI.requestFeedbackOnHangup = function () {
|
||||
if (Feedback.isVisible())
|
||||
return Promise.reject(FEEDBACK_REQUEST_IN_PROGRESS);
|
||||
// Feedback has been submitted already.
|
||||
else if (Feedback.isEnabled() && Feedback.isSubmitted()) {
|
||||
return Promise.resolve({
|
||||
thankYouDialogVisible : true,
|
||||
feedbackSubmitted: true
|
||||
});
|
||||
}
|
||||
else
|
||||
return new Promise(function (resolve) {
|
||||
if (Feedback.isEnabled()) {
|
||||
Feedback.openFeedbackWindow(
|
||||
(options) => {
|
||||
options.thankYouDialogVisible = false;
|
||||
resolve(options);
|
||||
});
|
||||
} else {
|
||||
// If the feedback functionality isn't enabled we show a thank
|
||||
// you dialog. Signaling it (true), so the caller
|
||||
// of requestFeedback can act on it
|
||||
resolve(
|
||||
{thankYouDialogVisible : true, feedbackSubmitted: false});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
UI.updateRecordingState = function (state) {
|
||||
Recording.updateRecordingState(state);
|
||||
};
|
||||
|
|
|
@ -1,95 +0,0 @@
|
|||
/* global $, APP, JitsiMeetJS */
|
||||
import FeedbackWindow from "./FeedbackWindow";
|
||||
|
||||
/**
|
||||
* Defines all methods in connection to the Feedback window.
|
||||
*
|
||||
* @type {{openFeedbackWindow: Function}}
|
||||
*/
|
||||
const Feedback = {
|
||||
|
||||
/**
|
||||
* Initialise the Feedback functionality.
|
||||
* @param emitter the EventEmitter to associate with the Feedback.
|
||||
*/
|
||||
init: function (emitter) {
|
||||
// CallStats is the way we send feedback, so we don't have to initialise
|
||||
// if callstats isn't enabled.
|
||||
if (!APP.conference.isCallstatsEnabled())
|
||||
return;
|
||||
|
||||
// If enabled property is still undefined, i.e. it hasn't been set from
|
||||
// some other module already, we set it to true by default.
|
||||
if (typeof this.enabled == "undefined")
|
||||
this.enabled = true;
|
||||
|
||||
this.window = new FeedbackWindow();
|
||||
this.emitter = emitter;
|
||||
|
||||
$("#feedbackButton").click(Feedback.openFeedbackWindow);
|
||||
},
|
||||
/**
|
||||
* Enables/ disabled the feedback feature.
|
||||
*/
|
||||
enableFeedback: function (enable) {
|
||||
this.enabled = enable;
|
||||
},
|
||||
|
||||
/**
|
||||
* Indicates if the feedback functionality is enabled.
|
||||
*
|
||||
* @return true if the feedback functionality is enabled, false otherwise.
|
||||
*/
|
||||
isEnabled: function() {
|
||||
return this.enabled && APP.conference.isCallstatsEnabled();
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns true if the feedback window is currently visible and false
|
||||
* otherwise.
|
||||
* @return {boolean} true if the feedback window is visible, false
|
||||
* otherwise
|
||||
*/
|
||||
isVisible: function() {
|
||||
return $(".feedback").is(":visible");
|
||||
},
|
||||
|
||||
/**
|
||||
* Indicates if the feedback is submitted.
|
||||
*
|
||||
* @return {boolean} {true} to indicate if the feedback is submitted,
|
||||
* {false} - otherwise
|
||||
*/
|
||||
isSubmitted: function() {
|
||||
return Feedback.window.submitted;
|
||||
},
|
||||
|
||||
/**
|
||||
* Opens the feedback window.
|
||||
*/
|
||||
openFeedbackWindow: function (callback) {
|
||||
Feedback.window.show(callback);
|
||||
|
||||
JitsiMeetJS.analytics.sendEvent('feedback.open');
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the feedback score.
|
||||
*
|
||||
* @returns {*}
|
||||
*/
|
||||
getFeedbackScore: function() {
|
||||
return Feedback.window.feedbackScore;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the feedback free text.
|
||||
*
|
||||
* @returns {null|*|message}
|
||||
*/
|
||||
getFeedbackText: function() {
|
||||
return Feedback.window.feedbackText;
|
||||
}
|
||||
};
|
||||
|
||||
export default Feedback;
|
|
@ -1,184 +0,0 @@
|
|||
/* global $, APP, interfaceConfig */
|
||||
|
||||
const labels = {
|
||||
1: 'Very Bad',
|
||||
2: 'Bad',
|
||||
3: 'Average',
|
||||
4: 'Good',
|
||||
5: 'Very Good'
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggles the appropriate css class for the given number of stars, to
|
||||
* indicate that those stars have been clicked/selected.
|
||||
*
|
||||
* @param starCount the number of stars, for which to toggle the css class
|
||||
*/
|
||||
function toggleStars(starCount) {
|
||||
let labelEl = $('#starLabel');
|
||||
let label = starCount >= 0 ?
|
||||
labels[starCount + 1] :
|
||||
'';
|
||||
|
||||
$('#stars > a').each(function(index, el) {
|
||||
if (index <= starCount) {
|
||||
el.classList.add("starHover");
|
||||
} else
|
||||
el.classList.remove("starHover");
|
||||
});
|
||||
labelEl.text(label);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the html for the rated feedback window.
|
||||
*
|
||||
* @returns {string} the contructed html string
|
||||
*/
|
||||
function createRateFeedbackHTML() {
|
||||
|
||||
let starClassName = (interfaceConfig.ENABLE_FEEDBACK_ANIMATION)
|
||||
? "icon-star-full shake-rotate"
|
||||
: "icon-star-full";
|
||||
|
||||
return `
|
||||
<form id="feedbackForm"
|
||||
action="javascript:false;" onsubmit="return false;">
|
||||
<div class="rating">
|
||||
<div class="star-label">
|
||||
<p id="starLabel"> </p>
|
||||
</div>
|
||||
<div id="stars" class="feedback-stars">
|
||||
<a class="star-btn">
|
||||
<i class=${ starClassName }></i>
|
||||
</a>
|
||||
<a class="star-btn">
|
||||
<i class=${ starClassName }></i>
|
||||
</a>
|
||||
<a class="star-btn">
|
||||
<i class=${ starClassName }></i>
|
||||
</a>
|
||||
<a class="star-btn">
|
||||
<i class=${ starClassName }></i>
|
||||
</a>
|
||||
<a class="star-btn">
|
||||
<i class=${ starClassName }></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="details">
|
||||
<textarea id="feedbackTextArea" class="input-control"
|
||||
data-i18n="[placeholder]dialog.feedbackHelp"></textarea>
|
||||
</div>
|
||||
</form>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Feedback is loaded callback
|
||||
* Calls when Modal window is in DOM
|
||||
*
|
||||
* @param Feedback
|
||||
*/
|
||||
let onLoadFunction = function (Feedback) {
|
||||
$('#stars > a').each((index, el) => {
|
||||
el.onmouseover = function(){
|
||||
toggleStars(index);
|
||||
};
|
||||
el.onmouseleave = function(){
|
||||
toggleStars(Feedback.feedbackScore - 1);
|
||||
};
|
||||
el.onclick = function(){
|
||||
Feedback.feedbackScore = index + 1;
|
||||
Feedback.setFeedbackMessage();
|
||||
};
|
||||
});
|
||||
|
||||
// Init stars to correspond to previously entered feedback.
|
||||
if (Feedback.feedbackScore > 0) {
|
||||
toggleStars(Feedback.feedbackScore - 1);
|
||||
}
|
||||
|
||||
if (Feedback.feedbackMessage && Feedback.feedbackMessage.length > 0)
|
||||
$('#feedbackTextArea').text(Feedback.feedbackMessage);
|
||||
|
||||
$('#feedbackTextArea').focus();
|
||||
};
|
||||
|
||||
/**
|
||||
* On Feedback Submitted callback
|
||||
*
|
||||
* @param Feedback
|
||||
*/
|
||||
function onFeedbackSubmitted(Feedback) {
|
||||
let form = $('#feedbackForm');
|
||||
let message = form.find('textarea').val();
|
||||
|
||||
APP.conference.sendFeedback(
|
||||
Feedback.feedbackScore,
|
||||
message);
|
||||
|
||||
// TODO: make sendFeedback return true or false.
|
||||
Feedback.submitted = true;
|
||||
|
||||
//Remove history is submitted
|
||||
Feedback.feedbackScore = -1;
|
||||
Feedback.feedbackMessage = '';
|
||||
Feedback.onHide();
|
||||
}
|
||||
|
||||
/**
|
||||
* On Feedback Closed callback
|
||||
*
|
||||
* @param Feedback
|
||||
*/
|
||||
function onFeedbackClosed(Feedback) {
|
||||
Feedback.onHide();
|
||||
}
|
||||
|
||||
/**
|
||||
* @class Dialog
|
||||
*
|
||||
*/
|
||||
export default class Dialog {
|
||||
|
||||
constructor() {
|
||||
this.feedbackScore = -1;
|
||||
this.feedbackMessage = '';
|
||||
this.submitted = false;
|
||||
this.onCloseCallback = function() {};
|
||||
|
||||
this.setDefaultOptions();
|
||||
}
|
||||
|
||||
setDefaultOptions() {
|
||||
var self = this;
|
||||
|
||||
this.options = {
|
||||
titleKey: 'dialog.rateExperience',
|
||||
msgString: createRateFeedbackHTML(),
|
||||
loadedFunction: function() {onLoadFunction(self);},
|
||||
submitFunction: function() {onFeedbackSubmitted(self);},
|
||||
closeFunction: function() {onFeedbackClosed(self);},
|
||||
wrapperClass: 'feedback',
|
||||
size: 'medium'
|
||||
};
|
||||
}
|
||||
|
||||
setFeedbackMessage() {
|
||||
this.feedbackMessage = $('#feedbackTextArea').val();
|
||||
}
|
||||
|
||||
show(cb) {
|
||||
const options = this.options;
|
||||
if (typeof cb === 'function') {
|
||||
this.onCloseCallback = cb;
|
||||
}
|
||||
|
||||
this.window = APP.UI.messageHandler.openTwoButtonDialog(options);
|
||||
}
|
||||
|
||||
onHide() {
|
||||
this.onCloseCallback({
|
||||
feedbackSubmitted: this.submitted
|
||||
});
|
||||
}
|
||||
}
|
|
@ -19,7 +19,6 @@ const logger = require("jitsi-meet-logger").getLogger(__filename);
|
|||
import UIEvents from "../../../service/UI/UIEvents";
|
||||
import UIUtil from '../util/UIUtil';
|
||||
import VideoLayout from '../videolayout/VideoLayout';
|
||||
import Feedback from '../feedback/Feedback.js';
|
||||
|
||||
import { setToolboxEnabled } from '../../../react/features/toolbox';
|
||||
import { setNotificationsEnabled } from '../../../react/features/notifications';
|
||||
|
@ -308,7 +307,6 @@ var Recording = {
|
|||
VideoLayout.enableDeviceAvailabilityIcons(
|
||||
APP.conference.getMyUserId(), false);
|
||||
VideoLayout.setLocalVideoVisible(false);
|
||||
Feedback.enableFeedback(false);
|
||||
APP.store.dispatch(setToolboxEnabled(false));
|
||||
APP.store.dispatch(setNotificationsEnabled(false));
|
||||
APP.UI.messageHandler.enablePopups(false);
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
/**
|
||||
* The type of the action which signals feedback was closed without submitting.
|
||||
*
|
||||
* {
|
||||
* type: CANCEL_FEEDBACK,
|
||||
* message: string,
|
||||
* score: number
|
||||
* }
|
||||
*/
|
||||
export const CANCEL_FEEDBACK = Symbol('CANCEL_FEEDBACK');
|
||||
|
||||
/**
|
||||
* The type of the action which signals feedback was submitted for recording.
|
||||
*
|
||||
* {
|
||||
* type: SUBMIT_FEEDBACK,
|
||||
* message: string,
|
||||
* score: number
|
||||
* }
|
||||
*/
|
||||
export const SUBMIT_FEEDBACK = Symbol('SUBMIT_FEEDBACK');
|
|
@ -0,0 +1,122 @@
|
|||
import { FEEDBACK_REQUEST_IN_PROGRESS } from '../../../modules/UI/UIErrors';
|
||||
|
||||
import { openDialog } from '../../features/base/dialog';
|
||||
|
||||
import {
|
||||
CANCEL_FEEDBACK,
|
||||
SUBMIT_FEEDBACK
|
||||
} from './actionTypes';
|
||||
import { FeedbackDialog } from './components';
|
||||
|
||||
declare var config: Object;
|
||||
declare var interfaceConfig: Object;
|
||||
|
||||
/**
|
||||
* Caches the passed in feedback in the redux store.
|
||||
*
|
||||
* @param {number} score - The quality score given to the conference.
|
||||
* @param {string} message - A description entered by the participant that
|
||||
* explains the rating.
|
||||
* @returns {{
|
||||
* type: CANCEL_FEEDBACK,
|
||||
* message: string,
|
||||
* score: number
|
||||
* }}
|
||||
*/
|
||||
export function cancelFeedback(score, message) {
|
||||
return {
|
||||
type: CANCEL_FEEDBACK,
|
||||
message,
|
||||
score
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Potentially open the {@code FeedbackDialog}. It will not be opened if it is
|
||||
* already open or feedback has already been submitted.
|
||||
*
|
||||
* @param {JistiConference} conference - The conference for which the feedback
|
||||
* would be about. The conference is passed in because feedback can occur after
|
||||
* a conference has been left, so references to it may no longer exist in redux.
|
||||
* @returns {Promise} Resolved with value - false if the dialog is enabled and
|
||||
* resolved with true if the dialog is disabled or the feedback was already
|
||||
* submitted. Rejected if another dialog is already displayed.
|
||||
*/
|
||||
export function maybeOpenFeedbackDialog(conference) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
|
||||
if (interfaceConfig.filmStripOnly || config.iAmRecorder) {
|
||||
// Intentionally fall through the if chain to prevent further action
|
||||
// from being taken with regards to showing feedback.
|
||||
} else if (state['features/base/dialog'].component === FeedbackDialog) {
|
||||
// Feedback is currently being displayed.
|
||||
|
||||
return Promise.reject(FEEDBACK_REQUEST_IN_PROGRESS);
|
||||
} else if (state['features/feedback'].submitted) {
|
||||
// Feedback has been submitted already.
|
||||
|
||||
return Promise.resolve({
|
||||
thankYouDialogVisible: true,
|
||||
feedbackSubmitted: true
|
||||
});
|
||||
} else if (conference.isCallstatsEnabled()) {
|
||||
return new Promise(resolve => {
|
||||
dispatch(openFeedbackDialog(conference, () => {
|
||||
const { submitted } = getState()['features/feedback'];
|
||||
|
||||
resolve({
|
||||
feedbackSubmitted: submitted,
|
||||
thankYouDialogVisible: false
|
||||
});
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
// If the feedback functionality isn't enabled we show a thank
|
||||
// you dialog. Signaling it (true), so the caller
|
||||
// of requestFeedback can act on it
|
||||
return Promise.resolve({
|
||||
thankYouDialogVisible: true,
|
||||
feedbackSubmitted: false
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens {@code FeedbackDialog}.
|
||||
*
|
||||
* @param {JitsiConference} conference - The JitsiConference that is being
|
||||
* rated. The conference is passed in because feedback can occur after a
|
||||
* conference has been left, so references to it may no longer exist in redux.
|
||||
* @param {Function} [onClose] - An optional callback to invoke when the dialog
|
||||
* is closed.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function openFeedbackDialog(conference, onClose) {
|
||||
return openDialog(FeedbackDialog, {
|
||||
conference,
|
||||
onClose
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the passed in feedback.
|
||||
*
|
||||
* @param {number} score - An integer between 1 and 5 indicating the user
|
||||
* feedback. The negative integer -1 is used to denote no score was selected.
|
||||
* @param {string} message - Detailed feedback from the user to explain the
|
||||
* rating.
|
||||
* @param {JitsiConference} conference - The JitsiConference for which the
|
||||
* feedback is being left.
|
||||
* @returns {{
|
||||
* type: SUBMIT_FEEDBACK
|
||||
* }}
|
||||
*/
|
||||
export function submitFeedback(score, message, conference) {
|
||||
conference.sendFeedback(score, message);
|
||||
|
||||
return {
|
||||
type: SUBMIT_FEEDBACK
|
||||
};
|
||||
}
|
|
@ -1,15 +1,23 @@
|
|||
/* @flow */
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
declare var config: Object;
|
||||
import { openFeedbackDialog } from '../actions';
|
||||
|
||||
/**
|
||||
* Implements a Web/React Component which renders a feedback button.
|
||||
*/
|
||||
export class FeedbackButton extends Component {
|
||||
state = {
|
||||
callStatsID: String
|
||||
class FeedbackButton extends Component {
|
||||
_onClick: Function;
|
||||
|
||||
static propTypes = {
|
||||
/**
|
||||
* The JitsiConference for which the feedback will be about.
|
||||
*
|
||||
* @type {JitsiConference}
|
||||
*/
|
||||
_conference: React.PropTypes.object
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -21,9 +29,8 @@ export class FeedbackButton extends Component {
|
|||
constructor(props: Object) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
callStatsID: config.callStatsID
|
||||
};
|
||||
// Bind event handlers so they are only bound once for every instance.
|
||||
this._onClick = this._onClick.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -33,15 +40,46 @@ export class FeedbackButton extends Component {
|
|||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
// If callstats.io-support is not configured, skip rendering.
|
||||
if (!this.state.callStatsID) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
className = 'button icon-feedback'
|
||||
id = 'feedbackButton' />
|
||||
id = 'feedbackButton'
|
||||
onClick = { this._onClick } />
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches an action to open a dialog requesting call feedback.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onClick() {
|
||||
const { _conference, dispatch } = this.props;
|
||||
|
||||
dispatch(openFeedbackDialog(_conference));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated Conference's props.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _toolboxVisible: boolean
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
return {
|
||||
/**
|
||||
* The JitsiConference for which the feedback will be about.
|
||||
*
|
||||
* @private
|
||||
* @type {JitsiConference}
|
||||
*/
|
||||
_conference: state['features/base/conference'].conference
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(FeedbackButton);
|
||||
|
|
|
@ -0,0 +1,343 @@
|
|||
import StarIcon from '@atlaskit/icon/glyph/star';
|
||||
import StarFilledIcon from '@atlaskit/icon/glyph/star-filled';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Dialog } from '../../base/dialog';
|
||||
import { translate } from '../../base/i18n';
|
||||
import JitsiMeetJS from '../../base/lib-jitsi-meet';
|
||||
|
||||
import { cancelFeedback, submitFeedback } from '../actions';
|
||||
|
||||
declare var interfaceConfig: Object;
|
||||
|
||||
const scoreAnimationClass = interfaceConfig.ENABLE_FEEDBACK_ANIMATION
|
||||
? 'shake-rotate' : '';
|
||||
|
||||
/**
|
||||
* The scores to display for selecting. The score is the index in the array and
|
||||
* the value of the index is a translation key used for display in the dialog.
|
||||
*
|
||||
* @types {string[]}
|
||||
*/
|
||||
const SCORES = [
|
||||
'feedback.veryBad',
|
||||
'feedback.bad',
|
||||
'feedback.average',
|
||||
'feedback.good',
|
||||
'feedback.veryGood'
|
||||
];
|
||||
|
||||
/**
|
||||
* A React {@code Component} for displaying a dialog to rate the current
|
||||
* conference quality, write a message describing the experience, and submit
|
||||
* the feedback.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
class FeedbackDialog extends Component {
|
||||
/**
|
||||
* {@code FeedbackDialog} component's property types.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static propTypes = {
|
||||
/**
|
||||
* The cached feedback message, if any, that was set when closing a
|
||||
* previous instance of {@code FeedbackDialog}.
|
||||
*/
|
||||
_message: React.PropTypes.string,
|
||||
|
||||
/**
|
||||
* The cached feedback score, if any, that was set when closing a
|
||||
* previous instance of {@code FeedbackDialog}.
|
||||
*/
|
||||
_score: React.PropTypes.number,
|
||||
|
||||
/**
|
||||
* The JitsiConference that is being rated. The conference is passed in
|
||||
* because feedback can occur after a conference has been left, so
|
||||
* references to it may no longer exist in redux.
|
||||
*
|
||||
* @type {JitsiConference}
|
||||
*/
|
||||
conference: React.PropTypes.object,
|
||||
|
||||
/**
|
||||
* Invoked to signal feedback submission or canceling.
|
||||
*/
|
||||
dispatch: React.PropTypes.func,
|
||||
|
||||
/**
|
||||
* Callback invoked when {@code FeedbackDialog} is unmounted.
|
||||
*/
|
||||
onClose: React.PropTypes.func,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: React.PropTypes.func
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes a new {@code FeedbackDialog} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only React {@code Component} props with
|
||||
* which the new instance is to be initialized.
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const { _message, _score } = this.props;
|
||||
|
||||
this.state = {
|
||||
/**
|
||||
* The currently entered feedback message.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
message: _message,
|
||||
|
||||
/**
|
||||
* The score selection index which is currently being hovered. The
|
||||
* value -1 is used as a sentinel value to match store behavior of
|
||||
* using -1 for no score having been selected.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
mousedOverScore: -1,
|
||||
|
||||
/**
|
||||
* The currently selected score selection index. The score will not
|
||||
* be 0 indexed so subtract one to map with SCORES.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
score: _score > -1 ? _score - 1 : _score
|
||||
};
|
||||
|
||||
/**
|
||||
* An array of objects with click handlers for each of the scores listed
|
||||
* in SCORES. This pattern is used for binding event handlers only once
|
||||
* for each score selection icon.
|
||||
*
|
||||
* @type {Object[]}
|
||||
*/
|
||||
this._scoreClickConfigurations = SCORES.map((textKey, index) => {
|
||||
return {
|
||||
_onClick: () => this._onScoreSelect(index),
|
||||
_onMouseOver: () => this._onScoreMouseOver(index)
|
||||
};
|
||||
});
|
||||
|
||||
// Bind event handlers so they are only bound once for every instance.
|
||||
this._onCancel = this._onCancel.bind(this);
|
||||
this._onMessageChange = this._onMessageChange.bind(this);
|
||||
this._onScoreContainerMouseLeave
|
||||
= this._onScoreContainerMouseLeave.bind(this);
|
||||
this._onSubmit = this._onSubmit.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits an analytics event to notify feedback has been opened.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidMount() {
|
||||
JitsiMeetJS.analytics.sendEvent('feedback.open');
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the onClose callback, if defined, to notify of the close event.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
if (this.props.onClose) {
|
||||
this.props.onClose();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { message, mousedOverScore, score } = this.state;
|
||||
const scoreToDisplayAsSelected
|
||||
= mousedOverScore > -1 ? mousedOverScore : score;
|
||||
|
||||
const scoreIcons = this._scoreClickConfigurations.map(
|
||||
(config, index) => {
|
||||
const isFilled = index <= scoreToDisplayAsSelected;
|
||||
const activeClass = isFilled ? 'active' : '';
|
||||
const className
|
||||
= `star-btn ${scoreAnimationClass} ${activeClass}`;
|
||||
|
||||
return (
|
||||
<a
|
||||
className = { className }
|
||||
key = { index }
|
||||
onClick = { config._onClick }
|
||||
onMouseOver = { config._onMouseOver }>
|
||||
{ isFilled
|
||||
? <StarFilledIcon
|
||||
label = 'star-filled'
|
||||
size = 'xlarge' />
|
||||
: <StarIcon
|
||||
label = 'star'
|
||||
size = 'xlarge' /> }
|
||||
</a>
|
||||
);
|
||||
});
|
||||
|
||||
const { t } = this.props;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
okTitleKey = 'dialog.Submit'
|
||||
onCancel = { this._onCancel }
|
||||
onSubmit = { this._onSubmit }
|
||||
titleKey = 'feedback.rateExperience'>
|
||||
<div className = 'feedback-dialog'>
|
||||
<div className = 'rating'>
|
||||
<div className = 'star-label'>
|
||||
<p id = 'starLabel'>
|
||||
{ t(SCORES[scoreToDisplayAsSelected]) }
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className = 'stars'
|
||||
onMouseLeave = { this._onScoreContainerMouseLeave }>
|
||||
{ scoreIcons }
|
||||
</div>
|
||||
</div>
|
||||
<div className = 'details'>
|
||||
<textarea
|
||||
autoFocus = { true }
|
||||
className = 'input-control'
|
||||
id = 'feedbackTextArea'
|
||||
onChange = { this._onMessageChange }
|
||||
placeholder = { t('dialog.feedbackHelp') }
|
||||
value = { message } />
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches an action notifying feedback was not submitted. The submitted
|
||||
* score will have one added as the rest of the app does not expect 0
|
||||
* indexing.
|
||||
*
|
||||
* @private
|
||||
* @returns {boolean} Returns true to close the dialog.
|
||||
*/
|
||||
_onCancel() {
|
||||
const { message, score } = this.state;
|
||||
const scoreToSubmit = score > -1 ? score + 1 : score;
|
||||
|
||||
this.props.dispatch(cancelFeedback(scoreToSubmit, message));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the known entered feedback message.
|
||||
*
|
||||
* @param {Object} event - The DOM event from updating the textfield for the
|
||||
* feedback message.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onMessageChange(event) {
|
||||
this.setState({ message: event.target.value });
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the currently selected score.
|
||||
*
|
||||
* @param {number} score - The index of the selected score in SCORES.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onScoreSelect(score) {
|
||||
this.setState({ score });
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the currently hovered score to null to indicate no hover is
|
||||
* occurring.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onScoreContainerMouseLeave() {
|
||||
this.setState({ mousedOverScore: -1 });
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the known state of the score icon currently behind hovered over.
|
||||
*
|
||||
* @param {number} mousedOverScore - The index of the SCORES value currently
|
||||
* being moused over.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onScoreMouseOver(mousedOverScore) {
|
||||
this.setState({ mousedOverScore });
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches the entered feedback for submission. The submitted score will
|
||||
* have one added as the rest of the app does not expect 0 indexing.
|
||||
*
|
||||
* @private
|
||||
* @returns {boolean} Returns true to close the dialog.
|
||||
*/
|
||||
_onSubmit() {
|
||||
const { conference, dispatch } = this.props;
|
||||
const { message, score } = this.state;
|
||||
|
||||
const scoreToSubmit = score > -1 ? score + 1 : score;
|
||||
|
||||
dispatch(submitFeedback(scoreToSubmit, message, conference));
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated {@code FeedbackDialog}'s
|
||||
* props.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {{
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
const { message, score } = state['features/feedback'];
|
||||
|
||||
return {
|
||||
/**
|
||||
* The cached feedback message, if any, that was set when closing a
|
||||
* previous instance of {@code FeedbackDialog}.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
_message: message,
|
||||
|
||||
/**
|
||||
* The currently selected score selection index.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
_score: score
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(FeedbackDialog));
|
|
@ -1 +1,2 @@
|
|||
export * from './FeedbackButton';
|
||||
export { default as FeedbackButton } from './FeedbackButton';
|
||||
export { default as FeedbackDialog } from './FeedbackDialog';
|
||||
|
|
|
@ -1 +1,5 @@
|
|||
export * from './actions';
|
||||
export * from './actionTypes';
|
||||
export * from './components';
|
||||
|
||||
import './reducer';
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
import {
|
||||
ReducerRegistry
|
||||
} from '../base/redux';
|
||||
|
||||
import {
|
||||
CANCEL_FEEDBACK,
|
||||
SUBMIT_FEEDBACK
|
||||
} from './actionTypes';
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
message: '',
|
||||
|
||||
// The sentinel value -1 is used to denote no rating has been set and to
|
||||
// preserve pre-redux behavior.
|
||||
score: -1,
|
||||
submitted: false
|
||||
};
|
||||
|
||||
/**
|
||||
* Reduces the Redux actions of the feature features/feedback.
|
||||
*/
|
||||
ReducerRegistry.register(
|
||||
'features/feedback',
|
||||
(state = DEFAULT_STATE, action) => {
|
||||
switch (action.type) {
|
||||
case CANCEL_FEEDBACK: {
|
||||
return {
|
||||
...state,
|
||||
message: action.message,
|
||||
score: action.score
|
||||
};
|
||||
}
|
||||
|
||||
case SUBMIT_FEEDBACK: {
|
||||
return {
|
||||
...state,
|
||||
message: '',
|
||||
score: -1,
|
||||
submitted: true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
|
@ -13,6 +13,7 @@ import { getToolbarClassNames } from '../functions';
|
|||
import Toolbar from './Toolbar';
|
||||
|
||||
declare var APP: Object;
|
||||
declare var config: Object;
|
||||
|
||||
/**
|
||||
* Implementation of secondary toolbar React component.
|
||||
|
@ -29,6 +30,12 @@ class SecondaryToolbar extends Component {
|
|||
* @static
|
||||
*/
|
||||
static propTypes = {
|
||||
/**
|
||||
* Application ID for callstats.io API. The {@code FeedbackButton} will
|
||||
* display if defined.
|
||||
*/
|
||||
_callStatsID: React.PropTypes.string,
|
||||
|
||||
/**
|
||||
* The indicator which determines whether the local participant is a
|
||||
* guest in the conference.
|
||||
|
@ -79,7 +86,7 @@ class SecondaryToolbar extends Component {
|
|||
* @returns {ReactElement}
|
||||
*/
|
||||
render(): ReactElement<*> | null {
|
||||
const { _secondaryToolbarButtons } = this.props;
|
||||
const { _callStatsID, _secondaryToolbarButtons } = this.props;
|
||||
|
||||
// The number of buttons to show in the toolbar isn't fixed, it depends
|
||||
// on the availability of features and configuration parameters. So
|
||||
|
@ -95,7 +102,7 @@ class SecondaryToolbar extends Component {
|
|||
className = { secondaryToolbarClassName }
|
||||
toolbarButtons = { _secondaryToolbarButtons }
|
||||
tooltipPosition = { 'right' }>
|
||||
<FeedbackButton />
|
||||
{ _callStatsID ? <FeedbackButton /> : null }
|
||||
</Toolbar>
|
||||
);
|
||||
}
|
||||
|
@ -140,8 +147,14 @@ function _mapDispatchToProps(dispatch: Function): Object {
|
|||
function _mapStateToProps(state: Object): Object {
|
||||
const { isGuest } = state['features/jwt'];
|
||||
const { secondaryToolbarButtons, visible } = state['features/toolbox'];
|
||||
const { callStatsID } = state['features/base/config'];
|
||||
|
||||
return {
|
||||
/**
|
||||
* Application ID for callstats.io API.
|
||||
*/
|
||||
_callStatsID: callStatsID,
|
||||
|
||||
/**
|
||||
* The indicator which determines whether the local participant is a
|
||||
* guest in the conference.
|
||||
|
|
Loading…
Reference in New Issue