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 { withPixelLineHeight } from '../../base/styles/functions.web';
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: {
...withPixelLineHeight(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'
}
},
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;
}
/**
* 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 }: 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 (
{isFilled
?
: }
);
});
return (
);
};
export default FeedbackDialog;