diff --git a/css/modals/feedback/_feedback.scss b/css/modals/feedback/_feedback.scss index 4320cc67c..118f9e213 100644 --- a/css/modals/feedback/_feedback.scss +++ b/css/modals/feedback/_feedback.scss @@ -44,61 +44,3 @@ -webkit-animation-timing-function: ease-in-out; animation-timing-function: ease-in-out } - -.feedback-dialog { - margin-bottom: 5px; - - .details { - 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 { - font-size: 14px; - height: 16px; - } - - .star-btn { - color: inherit; - cursor: pointer; - display: inline-block; - font-size: 34px; - outline: none; - position: relative; - text-decoration: none; - @include transition(all .2s ease); - - &.active, - &:hover, - &.starHover { - color: #36B37E; - }; - - } - .star-btn:focus, - .star-btn:active { - outline: 1px solid #B8C7E0; - } - } -} diff --git a/react/features/base/icons/svg/favorite-solid.svg b/react/features/base/icons/svg/favorite-solid.svg new file mode 100644 index 000000000..24b515ebd --- /dev/null +++ b/react/features/base/icons/svg/favorite-solid.svg @@ -0,0 +1,3 @@ + + + diff --git a/react/features/base/icons/svg/favorite.svg b/react/features/base/icons/svg/favorite.svg new file mode 100644 index 000000000..67587c9c0 --- /dev/null +++ b/react/features/base/icons/svg/favorite.svg @@ -0,0 +1,3 @@ + + + diff --git a/react/features/base/icons/svg/index.ts b/react/features/base/icons/svg/index.ts index 081bf61a0..48dd29ae1 100644 --- a/react/features/base/icons/svg/index.ts +++ b/react/features/base/icons/svg/index.ts @@ -39,6 +39,8 @@ export { default as IconExclamationSolid } from './exclamation-solid.svg'; export { default as IconExclamationTriangle } from './exclamation-triangle.svg'; export { default as IconExitFullscreen } from './exit-fullscreen.svg'; export { default as IconFaceSmile } from './face-smile.svg'; +export { default as IconFavorite } from './favorite.svg'; +export { default as IconFavoriteSolid } from './favorite-solid.svg'; export { default as IconFeedback } from './feedback.svg'; export { default as IconGear } from './gear.svg'; export { default as IconGoogle } from './google.svg'; diff --git a/react/features/feedback/components/FeedbackDialog.web.js b/react/features/feedback/components/FeedbackDialog.web.tsx similarity index 72% rename from react/features/feedback/components/FeedbackDialog.web.js rename to react/features/feedback/components/FeedbackDialog.web.tsx index 83dbb43a6..c35f2253f 100644 --- a/react/features/feedback/components/FeedbackDialog.web.js +++ b/react/features/feedback/components/FeedbackDialog.web.tsx @@ -1,23 +1,74 @@ -// @flow - -import StarIcon from '@atlaskit/icon/glyph/star'; -import StarFilledIcon from '@atlaskit/icon/glyph/star-filled'; +import { Theme } from '@mui/material'; +import { ClassNameMap, withStyles } from '@mui/styles'; import React, { Component } from 'react'; -import type { Dispatch } from 'redux'; +import { WithTranslation } from 'react-i18next'; +import { connect } from 'react-redux'; -import { - createFeedbackOpenEvent, - sendAnalytics -} from '../../analytics'; +import { createFeedbackOpenEvent } from '../../analytics/AnalyticsEvents'; +import { sendAnalytics } from '../../analytics/functions'; +import { IReduxState, IStore } from '../../app/types'; +import { IJitsiConference } from '../../base/conference/reducer'; import { isMobileBrowser } from '../../base/environment/utils'; -import { translate } from '../../base/i18n'; -import { connect } from '../../base/redux'; +import { translate } from '../../base/i18n/functions'; +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'; -declare var APP: Object; -declare var interfaceConfig: Object; +const styles = (theme: Theme) => { + return { + dialog: { + marginBottom: theme.spacing(1) + }, + + rating: { + display: 'flex', + flexDirection: 'column' 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' + } + } + }; +}; const scoreAnimationClass = interfaceConfig.ENABLE_FEEDBACK_ANIMATION ? 'shake-rotate' : ''; @@ -34,49 +85,51 @@ const SCORES = [ 'feedback.veryGood' ]; +const ICON_SIZE = 32; + type Scrollable = { - scroll: Function -} + scroll: Function; +}; /** * The type of the React {@code Component} props of {@link FeedbackDialog}. */ -type Props = { +interface IProps extends WithTranslation { /** * The cached feedback message, if any, that was set when closing a previous * instance of {@code FeedbackDialog}. */ - _message: string, + _message: string; /** * The cached feedback score, if any, that was set when closing a previous * instance of {@code FeedbackDialog}. */ - _score: number, + _score: number; + + /** + * An object containing the CSS classes. + */ + classes: ClassNameMap; /** * 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: Object, + conference: IJitsiConference; /** * Invoked to signal feedback submission or canceling. */ - dispatch: Dispatch, + dispatch: IStore['dispatch']; /** * Callback invoked when {@code FeedbackDialog} is unmounted. */ - onClose: Function, - - /** - * Invoked to obtain translated strings. - */ - t: Function -}; + onClose: Function; +} /** * The type of the React {@code Component} state of {@link FeedbackDialog}. @@ -86,20 +139,20 @@ type State = { /** * The currently entered feedback message. */ - message: string, + message: string; /** * 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. */ - mousedOverScore: number, + mousedOverScore: number; /** * The currently selected score selection index. The score will not be 0 * indexed so subtract one to map with SCORES. */ - score: number + score: number; }; /** @@ -109,13 +162,19 @@ type State = { * * @augments Component */ -class FeedbackDialog extends Component { +class FeedbackDialog extends Component { /** * 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. */ - _scoreClickConfigurations: Array; + _scoreClickConfigurations: Array<{ + _onClick: (e: React.MouseEvent) => void; + _onKeyDown: (e: React.KeyboardEvent) => void; + _onMouseOver: (e: React.MouseEvent) => void; + }>; + + _onScrollTop: (node: Scrollable | null) => void; /** * Initializes a new {@code FeedbackDialog} instance. @@ -123,7 +182,7 @@ class FeedbackDialog extends Component { * @param {Object} props - The read-only React {@code Component} props with * which the new instance is to be initialized. */ - constructor(props: Props) { + constructor(props: IProps) { super(props); const { _message, _score } = this.props; @@ -157,8 +216,9 @@ class FeedbackDialog extends Component { this._scoreClickConfigurations = SCORES.map((textKey, index) => { return { _onClick: () => this._onScoreSelect(index), - _onKeyPres: e => { + _onKeyDown: (e: React.KeyboardEvent) => { if (e.key === ' ' || e.key === 'Enter') { + e.stopPropagation(); e.preventDefault(); this._onScoreSelect(index); } @@ -176,8 +236,8 @@ class FeedbackDialog extends Component { // On some mobile browsers opening Feedback dialog scrolls down the whole content because of the keyboard. // By scrolling to the top we prevent hiding the feedback stars so the user knows those exist. - this._onScrollTop = (node: ?Scrollable) => { - node && node.scroll && node.scroll(0, 0); + this._onScrollTop = (node: Scrollable | null) => { + node?.scroll?.(0, 0); }; } @@ -215,14 +275,14 @@ class FeedbackDialog extends Component { const scoreToDisplayAsSelected = mousedOverScore > -1 ? mousedOverScore : score; - const { t } = this.props; + const { classes, t } = this.props; const scoreIcons = this._scoreClickConfigurations.map( (config, index) => { const isFilled = index <= scoreToDisplayAsSelected; const activeClass = isFilled ? 'active' : ''; const className - = `star-btn ${scoreAnimationClass} ${activeClass}`; + = `${classes.starBtn} ${scoreAnimationClass} ${activeClass}`; return ( { className = { className } key = { index } onClick = { config._onClick } - onKeyPress = { config._onKeyPres } + onKeyDown = { config._onKeyDown } role = 'button' tabIndex = { 0 } { ...(isMobileBrowser() ? {} : { onMouseOver: config._onMouseOver }) }> { isFilled - ? - : } + ? + : } ); }); @@ -255,23 +315,24 @@ class FeedbackDialog extends Component { }} onCancel = { this._onCancel } onSubmit = { this._onSubmit } + size = 'large' titleKey = 'feedback.rateExperience'> -
-
+
+
+ className = { classes.ratingLabel } >

{ t(SCORES[scoreToDisplayAsSelected]) }

{ scoreIcons }
-
+
{ ); } - _onCancel: () => boolean; - /** * 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 @@ -304,8 +363,6 @@ class FeedbackDialog extends Component { return true; } - _onMessageChange: (Object) => void; - /** * Updates the known entered feedback message. * @@ -314,7 +371,7 @@ class FeedbackDialog extends Component { * @private * @returns {void} */ - _onMessageChange(newValue) { + _onMessageChange(newValue: string) { this.setState({ message: newValue }); } @@ -325,12 +382,10 @@ class FeedbackDialog extends Component { * @private * @returns {void} */ - _onScoreSelect(score) { + _onScoreSelect(score: number) { this.setState({ score }); } - _onScoreContainerMouseLeave: () => void; - /** * Sets the currently hovered score to null to indicate no hover is * occurring. @@ -350,12 +405,10 @@ class FeedbackDialog extends Component { * @private * @returns {void} */ - _onScoreMouseOver(mousedOverScore) { + _onScoreMouseOver(mousedOverScore: number) { this.setState({ mousedOverScore }); } - _onSubmit: () => void; - /** * Dispatches the entered feedback for submission. The submitted score will * have one added as the rest of the app does not expect 0 indexing. @@ -373,8 +426,6 @@ class FeedbackDialog extends Component { return true; } - - _onScrollTop: (node: ?Scrollable) => void; } /** @@ -386,7 +437,7 @@ class FeedbackDialog extends Component { * @returns {{ * }} */ -function _mapStateToProps(state) { +function _mapStateToProps(state: IReduxState) { const { message, score } = state['features/feedback']; return { @@ -407,4 +458,4 @@ function _mapStateToProps(state) { }; } -export default translate(connect(_mapStateToProps)(FeedbackDialog)); +export default withStyles(styles)(translate(connect(_mapStateToProps)(FeedbackDialog)));