This commit is contained in:
29
react/features/feedback/actionTypes.ts
Normal file
29
react/features/feedback/actionTypes.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* The type of the action which signals feedback was closed without submitting.
|
||||
*
|
||||
* {
|
||||
* type: CANCEL_FEEDBACK,
|
||||
* message: string,
|
||||
* score: number
|
||||
* }
|
||||
*/
|
||||
export const CANCEL_FEEDBACK = 'CANCEL_FEEDBACK';
|
||||
|
||||
/**
|
||||
* The type of the action which signals feedback failed to be recorded.
|
||||
*
|
||||
* {
|
||||
* type: SUBMIT_FEEDBACK_ERROR
|
||||
* error: string
|
||||
* }
|
||||
*/
|
||||
export const SUBMIT_FEEDBACK_ERROR = 'SUBMIT_FEEDBACK_ERROR';
|
||||
|
||||
/**
|
||||
* The type of the action which signals feedback has been recorded.
|
||||
*
|
||||
* {
|
||||
* type: SUBMIT_FEEDBACK_SUCCESS,
|
||||
* }
|
||||
*/
|
||||
export const SUBMIT_FEEDBACK_SUCCESS = 'SUBMIT_FEEDBACK_SUCCESS';
|
||||
197
react/features/feedback/actions.web.ts
Normal file
197
react/features/feedback/actions.web.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
// @ts-expect-error
|
||||
import { FEEDBACK_REQUEST_IN_PROGRESS } from '../../../modules/UI/UIErrors';
|
||||
import { IStore } from '../app/types';
|
||||
import { IJitsiConference } from '../base/conference/reducer';
|
||||
import { openDialog } from '../base/dialog/actions';
|
||||
import { extractFqnFromPath } from '../dynamic-branding/functions.any';
|
||||
|
||||
import {
|
||||
CANCEL_FEEDBACK,
|
||||
SUBMIT_FEEDBACK_ERROR,
|
||||
SUBMIT_FEEDBACK_SUCCESS
|
||||
} from './actionTypes';
|
||||
import FeedbackDialog from './components/FeedbackDialog.web';
|
||||
import { sendFeedbackToJaaSRequest, shouldSendJaaSFeedbackMetadata } from './functions.web';
|
||||
|
||||
/**
|
||||
* 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: number, message: string) {
|
||||
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.
|
||||
* @param {string} title - The feedback dialog title.
|
||||
* @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: IJitsiConference, title?: string) {
|
||||
type R = {
|
||||
feedbackSubmitted: boolean;
|
||||
showThankYou: boolean;
|
||||
wasDialogShown: boolean;
|
||||
};
|
||||
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']): Promise<R> => {
|
||||
const state = getState();
|
||||
const { feedbackPercentage = 100 } = state['features/base/config'];
|
||||
|
||||
if (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({
|
||||
feedbackSubmitted: true,
|
||||
showThankYou: true,
|
||||
wasDialogShown: false
|
||||
});
|
||||
} else if (shouldSendJaaSFeedbackMetadata(state)
|
||||
&& feedbackPercentage > Math.random() * 100) {
|
||||
return new Promise(resolve => {
|
||||
dispatch(openFeedbackDialog(conference, title, () => {
|
||||
const { submitted } = getState()['features/feedback'];
|
||||
|
||||
resolve({
|
||||
feedbackSubmitted: submitted,
|
||||
showThankYou: false,
|
||||
wasDialogShown: true
|
||||
});
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
// If the feedback functionality isn't enabled we show a "thank you"
|
||||
// message. Signaling it (true), so the caller of requestFeedback can
|
||||
// act on it.
|
||||
return Promise.resolve({
|
||||
feedbackSubmitted: false,
|
||||
showThankYou: true,
|
||||
wasDialogShown: 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 {string} [title] - The feedback dialog title.
|
||||
* @param {Function} [onClose] - An optional callback to invoke when the dialog
|
||||
* is closed.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function openFeedbackDialog(conference?: IJitsiConference, title?: string, onClose?: Function) {
|
||||
return openDialog(FeedbackDialog, {
|
||||
conference,
|
||||
onClose,
|
||||
title
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends feedback metadata to JaaS endpoint.
|
||||
*
|
||||
* @param {JitsiConference} conference - The JitsiConference that is being rated.
|
||||
* @param {Object} feedback - The feedback message and score.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function sendJaasFeedbackMetadata(conference: IJitsiConference, feedback: Object) {
|
||||
return (_dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const state = getState();
|
||||
|
||||
if (!shouldSendJaaSFeedbackMetadata(state)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const { jaasFeedbackMetadataURL } = state['features/base/config'];
|
||||
const { jwt, user, tenant } = state['features/base/jwt'];
|
||||
const meetingFqn = extractFqnFromPath();
|
||||
const feedbackData = {
|
||||
...feedback,
|
||||
sessionId: conference.sessionId,
|
||||
userId: user?.id,
|
||||
meetingFqn,
|
||||
jwt,
|
||||
tenant
|
||||
};
|
||||
|
||||
return sendFeedbackToJaaSRequest(jaasFeedbackMetadataURL, feedbackData);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {Function}
|
||||
*/
|
||||
export function submitFeedback(
|
||||
score: number,
|
||||
message: string,
|
||||
conference: IJitsiConference) {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const state = getState();
|
||||
const promises = [];
|
||||
|
||||
if (shouldSendJaaSFeedbackMetadata(state)) {
|
||||
promises.push(dispatch(sendJaasFeedbackMetadata(conference, {
|
||||
score,
|
||||
message
|
||||
})));
|
||||
}
|
||||
|
||||
return Promise.allSettled(promises)
|
||||
.then(results => {
|
||||
const rejected = results.find((result): result is PromiseRejectedResult => result?.status === 'rejected');
|
||||
|
||||
if (typeof rejected === 'undefined') {
|
||||
dispatch({ type: SUBMIT_FEEDBACK_SUCCESS });
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const error = rejected.reason;
|
||||
|
||||
dispatch({
|
||||
type: SUBMIT_FEEDBACK_ERROR,
|
||||
error
|
||||
});
|
||||
|
||||
return Promise.reject(error);
|
||||
});
|
||||
};
|
||||
}
|
||||
57
react/features/feedback/components/FeedbackButton.web.ts
Normal file
57
react/features/feedback/components/FeedbackButton.web.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { createToolbarEvent } from '../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../analytics/functions';
|
||||
import { IReduxState } from '../../app/types';
|
||||
import { IJitsiConference } from '../../base/conference/reducer';
|
||||
import { translate } from '../../base/i18n/functions';
|
||||
import { IconFeedback } from '../../base/icons/svg';
|
||||
import AbstractButton, { IProps as AbstractButtonProps } from '../../base/toolbox/components/AbstractButton';
|
||||
import { openFeedbackDialog } from '../actions';
|
||||
import { shouldSendJaaSFeedbackMetadata } from '../functions.web';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link FeedbackButton}.
|
||||
*/
|
||||
interface IProps extends AbstractButtonProps {
|
||||
|
||||
/**
|
||||
* The {@code JitsiConference} for the current conference.
|
||||
*/
|
||||
_conference?: IJitsiConference;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of a button for opening feedback dialog.
|
||||
*/
|
||||
class FeedbackButton extends AbstractButton<IProps> {
|
||||
override accessibilityLabel = 'toolbar.accessibilityLabel.feedback';
|
||||
override icon = IconFeedback;
|
||||
override label = 'toolbar.feedback';
|
||||
override tooltip = 'toolbar.feedback';
|
||||
|
||||
/**
|
||||
* Handles clicking / pressing the button, and opens the appropriate dialog.
|
||||
*
|
||||
* @protected
|
||||
* @returns {void}
|
||||
*/
|
||||
override _handleClick() {
|
||||
const { _conference, dispatch } = this.props;
|
||||
|
||||
sendAnalytics(createToolbarEvent('feedback'));
|
||||
dispatch(openFeedbackDialog(_conference));
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: IReduxState) => {
|
||||
const { conference } = state['features/base/conference'];
|
||||
|
||||
return {
|
||||
_conference: conference,
|
||||
visible: shouldSendJaaSFeedbackMetadata(state)
|
||||
};
|
||||
};
|
||||
|
||||
export default translate(connect(mapStateToProps)(FeedbackButton));
|
||||
@@ -0,0 +1,3 @@
|
||||
import { Component } from 'react';
|
||||
|
||||
export default Component;
|
||||
330
react/features/feedback/components/FeedbackDialog.web.tsx
Normal file
330
react/features/feedback/components/FeedbackDialog.web.tsx
Normal file
@@ -0,0 +1,330 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { createFeedbackOpenEvent } from '../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../analytics/functions';
|
||||
import { IReduxState } from '../../app/types';
|
||||
import { IJitsiConference } from '../../base/conference/reducer';
|
||||
import { isMobileBrowser } from '../../base/environment/utils';
|
||||
import Icon from '../../base/icons/components/Icon';
|
||||
import { IconFavorite, IconFavoriteSolid } from '../../base/icons/svg';
|
||||
import Dialog from '../../base/ui/components/web/Dialog';
|
||||
import Input from '../../base/ui/components/web/Input';
|
||||
import { cancelFeedback, submitFeedback } from '../actions.web';
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
dialog: {
|
||||
marginBottom: theme.spacing(1)
|
||||
},
|
||||
|
||||
rating: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column-reverse' as const,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginTop: theme.spacing(4),
|
||||
marginBottom: theme.spacing(3)
|
||||
},
|
||||
|
||||
ratingLabel: {
|
||||
...theme.typography.bodyShortBold,
|
||||
color: theme.palette.text01,
|
||||
marginBottom: theme.spacing(2),
|
||||
height: '20px'
|
||||
},
|
||||
|
||||
stars: {
|
||||
display: 'flex'
|
||||
},
|
||||
|
||||
starBtn: {
|
||||
display: 'inline-block',
|
||||
cursor: 'pointer',
|
||||
marginRight: theme.spacing(3),
|
||||
|
||||
'&:last-of-type': {
|
||||
marginRight: 0
|
||||
},
|
||||
|
||||
'&.active svg': {
|
||||
fill: theme.palette.success01
|
||||
},
|
||||
|
||||
'&:focus': {
|
||||
outline: `1px solid ${theme.palette.action01}`,
|
||||
borderRadius: '4px'
|
||||
}
|
||||
},
|
||||
|
||||
title: {
|
||||
fontSize: '1rem'
|
||||
},
|
||||
|
||||
details: {
|
||||
'& textarea': {
|
||||
minHeight: '122px'
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
const SCORES = [
|
||||
'feedback.veryBad',
|
||||
'feedback.bad',
|
||||
'feedback.average',
|
||||
'feedback.good',
|
||||
'feedback.veryGood'
|
||||
];
|
||||
|
||||
const ICON_SIZE = 32;
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link FeedbackDialog}.
|
||||
*/
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
conference: IJitsiConference;
|
||||
|
||||
/**
|
||||
* Callback invoked when {@code FeedbackDialog} is unmounted.
|
||||
*/
|
||||
onClose: Function;
|
||||
|
||||
/**
|
||||
* The title to display in the dialog. Usually the reason that triggered the feedback.
|
||||
*/
|
||||
title?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A React {@code Component} for displaying a dialog to rate the current
|
||||
* conference quality, write a message describing the experience, and submit
|
||||
* the feedback.
|
||||
*
|
||||
* @param {IProps} props - Component's props.
|
||||
* @returns {JSX}
|
||||
*/
|
||||
const FeedbackDialog = ({ conference, onClose, title }: IProps) => {
|
||||
const { classes } = useStyles();
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const _message = useSelector((state: IReduxState) => state['features/feedback'].message);
|
||||
const _score = useSelector((state: IReduxState) => state['features/feedback'].score);
|
||||
|
||||
/**
|
||||
* The currently entered feedback message.
|
||||
*/
|
||||
const [ message, setMessage ] = useState(_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.
|
||||
*/
|
||||
const [ mousedOverScore, setMousedOverScore ] = useState(-1);
|
||||
|
||||
/**
|
||||
* The currently selected score selection index. The score will not
|
||||
* be 0 indexed so subtract one to map with SCORES.
|
||||
*/
|
||||
const [ score, setScore ] = useState(_score > -1 ? _score - 1 : _score);
|
||||
|
||||
/**
|
||||
* An array of objects with click handlers for each of the scores listed in
|
||||
* the constant SCORES. This pattern is used for binding event handlers only
|
||||
* once for each score selection icon.
|
||||
*/
|
||||
const scoreClickConfigurations = useRef(SCORES.map((textKey, index) => {
|
||||
return {
|
||||
_onClick: () => onScoreSelect(index),
|
||||
_onKeyDown: (e: React.KeyboardEvent) => {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
onScoreSelect(index);
|
||||
}
|
||||
},
|
||||
_onMouseOver: () => onScoreMouseOver(index)
|
||||
};
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
sendAnalytics(createFeedbackOpenEvent());
|
||||
if (typeof APP !== 'undefined') {
|
||||
APP.API.notifyFeedbackPromptDisplayed();
|
||||
}
|
||||
|
||||
return () => {
|
||||
onClose?.();
|
||||
};
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
const onCancel = useCallback(() => {
|
||||
const scoreToSubmit = score > -1 ? score + 1 : score;
|
||||
|
||||
dispatch(cancelFeedback(scoreToSubmit, message));
|
||||
|
||||
return true;
|
||||
}, [ score, message ]);
|
||||
|
||||
/**
|
||||
* Updates the known entered feedback message.
|
||||
*
|
||||
* @param {string} newValue - The new value from updating the textfield for the
|
||||
* feedback message.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
const onMessageChange = useCallback((newValue: string) => {
|
||||
setMessage(newValue);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Updates the currently selected score.
|
||||
*
|
||||
* @param {number} newScore - The index of the selected score in SCORES.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function onScoreSelect(newScore: number) {
|
||||
setScore(newScore);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the currently hovered score to null to indicate no hover is
|
||||
* occurring.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
const onScoreContainerMouseLeave = useCallback(() => {
|
||||
setMousedOverScore(-1);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Updates the known state of the score icon currently behind hovered over.
|
||||
*
|
||||
* @param {number} newMousedOverScore - The index of the SCORES value currently
|
||||
* being moused over.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function onScoreMouseOver(newMousedOverScore: number) {
|
||||
setMousedOverScore(newMousedOverScore);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
const _onSubmit = useCallback(() => {
|
||||
const scoreToSubmit = score > -1 ? score + 1 : score;
|
||||
|
||||
dispatch(submitFeedback(scoreToSubmit, message, conference));
|
||||
|
||||
return true;
|
||||
}, [ score, message, conference ]);
|
||||
|
||||
const scoreToDisplayAsSelected
|
||||
= mousedOverScore > -1 ? mousedOverScore : score;
|
||||
|
||||
const scoreIcons = scoreClickConfigurations.current.map(
|
||||
(config, index) => {
|
||||
const isFilled = index <= scoreToDisplayAsSelected;
|
||||
const activeClass = isFilled ? 'active' : '';
|
||||
const className
|
||||
= `${classes.starBtn} ${activeClass}`;
|
||||
|
||||
return (
|
||||
<span
|
||||
aria-label = { t(SCORES[index]) }
|
||||
className = { className }
|
||||
key = { index }
|
||||
onClick = { config._onClick }
|
||||
onKeyDown = { config._onKeyDown }
|
||||
role = 'button'
|
||||
tabIndex = { 0 }
|
||||
{ ...(isMobileBrowser() ? {} : {
|
||||
onMouseOver: config._onMouseOver
|
||||
}) }>
|
||||
{isFilled
|
||||
? <Icon
|
||||
size = { ICON_SIZE }
|
||||
src = { IconFavoriteSolid } />
|
||||
: <Icon
|
||||
size = { ICON_SIZE }
|
||||
src = { IconFavorite } />}
|
||||
</span>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
disableEnter = { true }
|
||||
ok = {{
|
||||
translationKey: 'dialog.Submit'
|
||||
}}
|
||||
onCancel = { onCancel }
|
||||
onSubmit = { _onSubmit }
|
||||
size = 'large'
|
||||
titleKey = 'feedback.rateExperience'>
|
||||
<div className = { classes.dialog }>
|
||||
{title ? <div className = { classes.title }>{t(title)}</div> : null}
|
||||
<div className = { classes.rating }>
|
||||
<div
|
||||
className = { classes.stars }
|
||||
onMouseLeave = { onScoreContainerMouseLeave }>
|
||||
{scoreIcons}
|
||||
</div>
|
||||
<div
|
||||
className = { classes.ratingLabel } >
|
||||
<p className = 'sr-only'>
|
||||
{t('feedback.accessibilityLabel.yourChoice', {
|
||||
rating: t(SCORES[scoreToDisplayAsSelected])
|
||||
})}
|
||||
</p>
|
||||
<p
|
||||
aria-hidden = { true }
|
||||
id = 'starLabel'>
|
||||
{t(SCORES[scoreToDisplayAsSelected])}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className = { classes.details }>
|
||||
<Input
|
||||
id = 'feedbackTextArea'
|
||||
label = { t('feedback.detailsLabel') }
|
||||
onChange = { onMessageChange }
|
||||
textarea = { true }
|
||||
value = { message } />
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeedbackDialog;
|
||||
70
react/features/feedback/functions.web.ts
Normal file
70
react/features/feedback/functions.web.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { IReduxState } from '../app/types';
|
||||
import { isVpaasMeeting } from '../jaas/functions';
|
||||
|
||||
import logger from './logger';
|
||||
|
||||
/**
|
||||
* Sends feedback metadata to JaaS endpoints.
|
||||
*
|
||||
* @param {string|undefined} url - The JaaS metadata endpoint URL.
|
||||
* @param {Object} feedbackData - The feedback data object.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export async function sendFeedbackToJaaSRequest(url: string | undefined, feedbackData: {
|
||||
jwt?: string; meetingFqn: string; message?: string; score?: number;
|
||||
sessionId: string; tenant?: string; userId?: string;
|
||||
}) {
|
||||
if (!url) {
|
||||
throw new TypeError('Trying to send jaas feedback request to an undefined URL!');
|
||||
}
|
||||
|
||||
const {
|
||||
jwt,
|
||||
sessionId,
|
||||
meetingFqn,
|
||||
score,
|
||||
message,
|
||||
userId,
|
||||
tenant
|
||||
} = feedbackData;
|
||||
const headers = {
|
||||
'Authorization': `Bearer ${jwt}`,
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
const data = {
|
||||
sessionId,
|
||||
meetingFqn,
|
||||
userId,
|
||||
tenant,
|
||||
submitted: new Date().getTime(),
|
||||
rating: score,
|
||||
comments: message
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
logger.error('Status error:', res.status);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('Could not send request', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether jaas feedback metadata should be send or not.
|
||||
*
|
||||
* @param {IReduxState} state - The redux state.
|
||||
* @returns {boolean} - True if jaas feedback metadata should be send and false otherwise.
|
||||
*/
|
||||
export function shouldSendJaaSFeedbackMetadata(state: IReduxState) {
|
||||
const { jaasFeedbackMetadataURL } = state['features/base/config'];
|
||||
|
||||
return Boolean(isVpaasMeeting(state) && jaasFeedbackMetadataURL);
|
||||
}
|
||||
|
||||
23
react/features/feedback/hooks.web.ts
Normal file
23
react/features/feedback/hooks.web.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import FeedbackButtonWeb from './components/FeedbackButton.web';
|
||||
import { shouldSendJaaSFeedbackMetadata } from './functions.web';
|
||||
|
||||
const feedback = {
|
||||
key: 'feedback',
|
||||
Content: FeedbackButtonWeb,
|
||||
group: 4
|
||||
};
|
||||
|
||||
/**
|
||||
* A hook that returns the feedback button if it is enabled and undefined otherwise.
|
||||
*
|
||||
* @returns {Object | undefined}
|
||||
*/
|
||||
export function useFeedbackButton() {
|
||||
const visible = useSelector(shouldSendJaaSFeedbackMetadata);
|
||||
|
||||
if (visible) {
|
||||
return feedback;
|
||||
}
|
||||
}
|
||||
3
react/features/feedback/logger.ts
Normal file
3
react/features/feedback/logger.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { getLogger } from '../base/logging/functions';
|
||||
|
||||
export default getLogger('features/feedback');
|
||||
51
react/features/feedback/reducer.ts
Normal file
51
react/features/feedback/reducer.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import ReducerRegistry from '../base/redux/ReducerRegistry';
|
||||
|
||||
import {
|
||||
CANCEL_FEEDBACK,
|
||||
SUBMIT_FEEDBACK_ERROR,
|
||||
SUBMIT_FEEDBACK_SUCCESS
|
||||
} 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
|
||||
};
|
||||
|
||||
export interface IFeedbackState {
|
||||
message: string;
|
||||
score: number;
|
||||
submitted: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduces the Redux actions of the feature features/feedback.
|
||||
*/
|
||||
ReducerRegistry.register<IFeedbackState>(
|
||||
'features/feedback',
|
||||
(state = DEFAULT_STATE, action): IFeedbackState => {
|
||||
switch (action.type) {
|
||||
case CANCEL_FEEDBACK: {
|
||||
return {
|
||||
...state,
|
||||
message: action.message,
|
||||
score: action.score
|
||||
};
|
||||
}
|
||||
|
||||
case SUBMIT_FEEDBACK_ERROR:
|
||||
case SUBMIT_FEEDBACK_SUCCESS: {
|
||||
return {
|
||||
...state,
|
||||
message: '',
|
||||
score: -1,
|
||||
submitted: true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
||||
Reference in New Issue
Block a user