From 1ad8f7717988b60ea6387c16a403ee00c034bd3a Mon Sep 17 00:00:00 2001 From: Vlad Piersec Date: Fri, 8 Oct 2021 11:05:16 +0300 Subject: [PATCH] feat(dialogs): Redesign Video Quality dialog & change dialog background color --- css/_atlaskit_overrides.scss | 55 ------ css/main.scss | 1 - css/modals/_dialog.scss | 3 - css/modals/video-quality/_video-quality.scss | 114 ------------ lang/main.json | 3 + react/features/app/components/App.web.js | 2 + .../base/dialog/components/web/ModalHeader.js | 63 ++++++- .../dialog/components/web/StatelessDialog.js | 25 ++- .../base/ui/components/BaseTheme.web.js | 8 +- .../base/ui/components/GlobalStyles.js | 35 ++++ react/features/base/ui/constants.js | 41 +++++ react/features/base/ui/functions.web.js | 16 ++ .../components/SpeakerStatsSearch.js | 5 +- .../video-quality/components/Slider.js | 169 ++++++++++++++++++ .../components/VideoQualityDialog.web.js | 4 +- .../components/VideoQualitySlider.web.js | 120 ++++++------- 16 files changed, 411 insertions(+), 253 deletions(-) delete mode 100644 css/modals/video-quality/_video-quality.scss create mode 100644 react/features/base/ui/components/GlobalStyles.js create mode 100644 react/features/base/ui/constants.js create mode 100644 react/features/video-quality/components/Slider.js diff --git a/css/_atlaskit_overrides.scss b/css/_atlaskit_overrides.scss index 1261dc772..c4a3ca597 100644 --- a/css/_atlaskit_overrides.scss +++ b/css/_atlaskit_overrides.scss @@ -24,61 +24,6 @@ bottom: calc(#{$newToolbarSizeWithPadding}) !important; } -.modal-dialog-form { - /** - * Override @atlaskit/dropdown-menu styling when in a modal because the - * dropdown backgrounds clash with the modal backgrounds. - */ - .dropdown-menu div[style*="transform"] { - outline: 1px solid #455166; - } - .dropdown-menu button:not(:active):not(:hover) > span { - color: #B8C7E0; - } - - /** - * Override @atlaskit/tab styling when in a modal because the - * tab text color clash with the modal backgrounds. - */ - div[role="tablist"] > div:not([data-selected]):not(:hover), - label > div > span { - color: #B8C7E0 !important; - } -} - -/** - * Override @atlaskit/modal-dialog header styling - */ -.atlaskit-portal [role="dialog"] header { - box-shadow: none; - .jitsi-icon { - cursor: pointer; - } - - .jitsi-icon svg { - fill: #B8C7E0; - } -} - -/** - * Override @atlaskit/modal-dialog footer styling. - */ - .atlaskit-portal [role="dialog"] footer { - box-shadow: none; - } - -/** - * Make header close button more easily tappable on mobile. - */ -.mobile-browser .atlaskit-portal [role="dialog"] header .jitsi-icon { - display: grid; - place-items: center; - height: 48px; - width: 48px; - background: #2a3a4b; - border-radius: 3px; -} - /** * Override @atlaskit/theme styling for the top toolbar so it displays over * the video thumbnail while obscuring as little as possible. diff --git a/css/main.scss b/css/main.scss index ed08fa42b..d9ec0d612 100644 --- a/css/main.scss +++ b/css/main.scss @@ -44,7 +44,6 @@ $flagsImagePath: "../images/"; @import 'modals/screen-share/share-audio'; @import 'modals/screen-share/share-screen-warning'; @import 'modals/speaker_stats/speaker_stats'; -@import 'modals/video-quality/video-quality'; @import 'modals/virtual-background/virtual-background'; @import 'modals/local-recording/local-recording'; @import 'videolayout_default'; diff --git a/css/modals/_dialog.scss b/css/modals/_dialog.scss index fa5e24069..4a8e1f2c6 100644 --- a/css/modals/_dialog.scss +++ b/css/modals/_dialog.scss @@ -122,9 +122,6 @@ margin-bottom: 8px; } } -.modal-dialog-footer { - font-size: $modalButtonFontSize; -} /** * Styling inline dialog errors. diff --git a/css/modals/video-quality/_video-quality.scss b/css/modals/video-quality/_video-quality.scss deleted file mode 100644 index 855f584b3..000000000 --- a/css/modals/video-quality/_video-quality.scss +++ /dev/null @@ -1,114 +0,0 @@ -.video-quality-dialog { - .video-quality-dialog-title { - margin-bottom: 10px; - } - - .video-quality-dialog-contents { - align-items: center; - display: flex; - flex-direction: column; - padding: 10px; - min-width: 250px; - - .video-quality-dialog-slider-container { - width: 100%; - text-align: center; - } - - .video-quality-dialog-slider { - width: calc(100% - 5px); - - @mixin sliderTrackStyles() { - height: 15px; - border-radius: 10px; - background: rgb(14, 22, 36); - } - - &::-ms-track { - @include sliderTrackStyles(); - } - - &::-moz-range-track { - @include sliderTrackStyles(); - } - - &::-webkit-slider-runnable-track { - @include sliderTrackStyles(); - } - - @mixin sliderThumbStyles() { - top: 50%; - border: none; - position: relative; - opacity: 0; - } - - &::-ms-thumb { - @include sliderThumbStyles(); - } - - &::-moz-range-thumb { - @include sliderThumbStyles(); - - } - - &::-webkit-slider-thumb { - @include sliderThumbStyles(); - } - } - - .video-quality-dialog-labels { - box-sizing: border-box; - display: flex; - margin-top: 5px; - position: relative; - width: 90%; - } - - .video-quality-dialog-label-container { - position: absolute; - text-align: center; - transform: translate(-50%, 0%); - - &::before { - content: ''; - border-radius: 50%; - left: 0; - height: 6px; - margin: 0 auto; - pointer-events: none; - position: absolute; - right: 0; - top: -16px; - width: 6px; - } - } - - .video-quality-dialog-label-container.active { - color: $videoQualityActive; - font-weight: bold; - - &::before { - background: $videoQualityActive; - height: 12px; - top: -19px; - width: 12px; - } - } - - .video-quality-dialog-label-container:first-child { - position: relative; - } - - .video-quality-dialog-label { - display: table-caption; - word-spacing: unset; - } - } -} - -.modal-dialog-form { - .video-quality-dialog-title { - display: none; - } -} diff --git a/lang/main.json b/lang/main.json index 61520e00a..639f000a3 100644 --- a/lang/main.json +++ b/lang/main.json @@ -1045,7 +1045,10 @@ "pending": "{{displayName}} has been invited" }, "videoStatus": { + "adjustFor": "Adjust for:", "audioOnly": "AUD", + "bestPerformance": "Best performance", + "highestQuality": "Highest quality", "audioOnlyExpanded": "You are in low bandwidth mode. In this mode you will receive only audio and screen sharing.", "callQuality": "Video Quality", "hd": "HD", diff --git a/react/features/app/components/App.web.js b/react/features/app/components/App.web.js index b8c03a458..ffc6aa9d5 100644 --- a/react/features/app/components/App.web.js +++ b/react/features/app/components/App.web.js @@ -4,6 +4,7 @@ import { AtlasKitThemeProvider } from '@atlaskit/theme'; import React from 'react'; import { DialogContainer } from '../../base/dialog'; +import GlobalStyles from '../../base/ui/components/GlobalStyles'; import JitsiThemeProvider from '../../base/ui/components/JitsiThemeProvider'; import { ChromeExtensionBanner } from '../../chrome-extension-banner'; @@ -29,6 +30,7 @@ export class App extends AbstractApp { return ( + { super._createMainElement(component, props) } diff --git a/react/features/base/dialog/components/web/ModalHeader.js b/react/features/base/dialog/components/web/ModalHeader.js index a8e9a6086..fd702bbb2 100644 --- a/react/features/base/dialog/components/web/ModalHeader.js +++ b/react/features/base/dialog/components/web/ModalHeader.js @@ -8,10 +8,12 @@ import { titleIconWrapperStyles, TitleText } from '@atlaskit/modal-dialog/dist/es2019/styled/Content'; +import { withStyles } from '@material-ui/core/styles'; import React from 'react'; import { translate } from '../../../i18n'; import { Icon, IconClose } from '../../../icons'; +import { withPixelLineHeight } from '../../../styles/functions'; const TitleIcon = ({ appearance }: { appearance?: 'danger' | 'warning' }) => { if (!appearance) { @@ -30,6 +32,7 @@ const TitleIcon = ({ appearance }: { appearance?: 'danger' | 'warning' }) => { type Props = { id: string, appearance?: 'danger' | 'warning', + classes: Object, heading: string, hideCloseIconButton: boolean, onClose: Function, @@ -39,6 +42,40 @@ type Props = { t: Function } +/** + * Creates the styles for the component. + * + * @param {Object} theme - The current UI theme. + * + * @returns {Object} + */ +const styles = theme => { + return { + closeButton: { + borderRadius: theme.shape.borderRadius, + cursor: 'pointer', + padding: 13, + + [theme.breakpoints.down('480')]: { + background: theme.palette.action02 + }, + + '&:hover': { + background: theme.palette.action02 + } + }, + header: { + boxShadow: 'none', + + '& h4': { + ...withPixelLineHeight(theme.typography.heading5), + color: theme.palette.text01 + } + } + }; +}; + + /** * A default header for modal-dialog components * @@ -90,6 +127,7 @@ class ModalHeader extends React.Component { const { id, appearance, + classes, heading, hideCloseIconButton, onClose, @@ -104,7 +142,9 @@ class ModalHeader extends React.Component { } return ( -
+
<TitleIcon appearance = { appearance } /> <TitleText @@ -116,16 +156,21 @@ class ModalHeader extends React.Component<Props> { { - !hideCloseIconButton && + !hideCloseIconButton + && }
); } } -export default translate(ModalHeader); +export default translate(withStyles(styles)(ModalHeader)); diff --git a/react/features/base/dialog/components/web/StatelessDialog.js b/react/features/base/dialog/components/web/StatelessDialog.js index f656850c6..62571388d 100644 --- a/react/features/base/dialog/components/web/StatelessDialog.js +++ b/react/features/base/dialog/components/web/StatelessDialog.js @@ -3,6 +3,7 @@ import ButtonGroup from '@atlaskit/button/button-group'; import Button from '@atlaskit/button/standard-button'; import Modal, { ModalFooter } from '@atlaskit/modal-dialog'; +import { withStyles } from '@material-ui/core/styles'; import _ from 'lodash'; import React, { Component } from 'react'; @@ -30,6 +31,11 @@ const OK_BUTTON_ID = 'modal-dialog-ok-button'; */ type Props = DialogProps & { + /** + * An object containing the CSS classes. + */ + classes: Object, + /** * Custom dialog header that replaces the standard heading. */ @@ -101,6 +107,19 @@ type Props = DialogProps & { width: string }; +/** + * Creates the styles for the component. + * + * @returns {Object} + */ +const styles = () => { + return { + footer: { + boxShadow: 'none' + } + }; +}; + /** * Web dialog that uses atlaskit modal-dialog to display dialogs. */ @@ -205,7 +224,9 @@ class StatelessDialog extends Component { } return ( - + { /** @@ -367,4 +388,4 @@ class StatelessDialog extends Component { } } -export default translate(StatelessDialog); +export default translate(withStyles(styles)(StatelessDialog)); diff --git a/react/features/base/ui/components/BaseTheme.web.js b/react/features/base/ui/components/BaseTheme.web.js index 687418ddc..ce05b7bf9 100644 --- a/react/features/base/ui/components/BaseTheme.web.js +++ b/react/features/base/ui/components/BaseTheme.web.js @@ -9,5 +9,11 @@ export default createWebTheme({ colorMap, spacing, shape, - typography + typography, + breakpoints: { + values: { + '0': 0, + '480': 480 + } + } }); diff --git a/react/features/base/ui/components/GlobalStyles.js b/react/features/base/ui/components/GlobalStyles.js new file mode 100644 index 000000000..dc664bd6c --- /dev/null +++ b/react/features/base/ui/components/GlobalStyles.js @@ -0,0 +1,35 @@ +// @flow + +import { createStyles, makeStyles } from '@material-ui/core'; + +import { commonStyles, getGlobalStyles } from '../constants'; +import { formatCommonClasses } from '../functions'; + +/** + * Creates all the global styles. + * + * @param {Object} theme - The current UI theme. + * + * @returns {Object} + */ +const useStyles = makeStyles(theme => + createStyles({ + '@global': { + ...formatCommonClasses(commonStyles), + ...getGlobalStyles(theme) + } + }) +); + +/** + * A component generating all the global styles. + * + * @returns {void} + */ +function GlobalStyles() { + useStyles(); + + return null; +} + +export default GlobalStyles; diff --git a/react/features/base/ui/constants.js b/react/features/base/ui/constants.js new file mode 100644 index 000000000..b5137a999 --- /dev/null +++ b/react/features/base/ui/constants.js @@ -0,0 +1,41 @@ +// @flow + +/** + * An object containing all the class names for common CSS. + * Add a new name here and the styles to {@code commonStyles} object. + * + */ +export const commonClassName = { + emptyList: 'empty-list' +}; + +/** + * An object containing the declaration of the common, reusable CSS classes. + */ +export const commonStyles = { + // '.empty-list' + [commonClassName.emptyList]: { + listStyleType: 'none', + margin: 0, + padding: 0 + } +}; + +/** + * Returns the global styles. + * + * @param {Object} theme - The Jitsi theme. + * @returns {Object} + */ +export const getGlobalStyles = (theme: Object) => { + return { + // @atlaskit/modal-dialog OVERRIDES + '.atlaskit-portal div[role=dialog]': { + // override dialog background + '& > div': { + background: theme.palette.ui02, + color: theme.palette.text01 + } + } + }; +}; diff --git a/react/features/base/ui/functions.web.js b/react/features/base/ui/functions.web.js index 713ad0faf..c433b37ce 100644 --- a/react/features/base/ui/functions.web.js +++ b/react/features/base/ui/functions.web.js @@ -30,3 +30,19 @@ export function createWebTheme({ font, colors, colorMap, shape, spacing, typogra } }); } + +/** + * Formats the common styles object to be interpreted as proper CSS. + * + * @param {Object} stylesObj - The styles object. + * @returns {Object} + */ +export function formatCommonClasses(stylesObj: Object) { + const formatted = {}; + + for (const [ key, value ] of Object.entries(stylesObj)) { + formatted[`.${key}`] = value; + } + + return formatted; +} diff --git a/react/features/speaker-stats/components/SpeakerStatsSearch.js b/react/features/speaker-stats/components/SpeakerStatsSearch.js index 2f2cbcafa..34adc9017 100644 --- a/react/features/speaker-stats/components/SpeakerStatsSearch.js +++ b/react/features/speaker-stats/components/SpeakerStatsSearch.js @@ -13,8 +13,8 @@ const useStyles = makeStyles(() => { return { speakerStatsSearch: { position: 'absolute', - right: '50px', - top: '-5px' + right: '80px', + top: '8px' } }; }); @@ -70,4 +70,3 @@ function SpeakerStatsSearch({ onSearch }: Props) { } export default SpeakerStatsSearch; - diff --git a/react/features/video-quality/components/Slider.js b/react/features/video-quality/components/Slider.js new file mode 100644 index 000000000..0eec69a94 --- /dev/null +++ b/react/features/video-quality/components/Slider.js @@ -0,0 +1,169 @@ +// @flow +import { makeStyles } from '@material-ui/core/styles'; +import clsx from 'clsx'; +import React from 'react'; + +import { commonClassName } from '../../base/ui/constants'; + +type Props = { + + /** + * The 'aria-label' text. + */ + ariaLabel: string, + + /** + * The maximum value for slider value. + */ + max: number, + + /** + * The minimum value for slider value. + */ + min: number, + + /** + * Callback invoked on change. + */ + onChange: Function, + + /** + * The granularity that the value must adhere to. + */ + step: number, + + /** + * The current value where the knob is positioned. + */ + value: number +} + +const useStyles = makeStyles(theme => { + // keep the same hight for all elements: + // input, input track & fake track(div) + const height = 6; + + const inputTrack = { + background: 'transparent', + height + }; + const inputThumb = { + background: theme.palette.text01, + border: 0, + borderRadius: '50%', + height: 24, + width: 24 + }; + + const focused = { + outline: `1px solid ${theme.palette.action03Focus}` + }; + + return { + sliderContainer: { + cursor: 'pointer', + width: '100%', + position: 'relative', + textAlign: 'center' + + }, + knobContainer: { + display: 'flex', + justifyContent: 'space-between', + marginLeft: 2, + marginRight: 2, + position: 'absolute', + width: '100%' + }, + knob: { + background: theme.palette.text01, + borderRadius: '50%', + display: 'inline-block', + height, + width: 6 + }, + track: { + background: theme.palette.ui02, + borderRadius: theme.shape.borderRadius / 2, + height + }, + slider: { + // Use an additional class here to override global CSS specificity + '&.custom-slider': { + '-webkit-appearance': 'none', + background: 'transparent', + left: 0, + position: 'absolute', + top: 0, + width: '100%', + + '&:focus': { + // override global styles in order to use our own color + outline: 'none !important', + + '&::-webkit-slider-runnable-track': focused, + '&::ms-track': focused, + '&::-moz-range-track': focused + }, + + '&::-webkit-slider-runnable-track': { + '-webkit-appearance': 'none', + ...inputTrack + }, + '&::-webkit-slider-thumb': { + '-webkit-appearance': 'none', + position: 'relative', + top: -6, + ...inputThumb + }, + + '&::ms-track': { + ...inputTrack + }, + '&::-ms-thumb': { + ...inputThumb + }, + + '&::-moz-range-track': { + ...inputTrack + }, + '&::-moz-range-thumb': { + ...inputThumb + } + } + } + }; +}); + +/** + * Custom slider. + * + * @returns {ReactElement} + */ +function Slider({ ariaLabel, max, min, onChange, step, value }: Props) { + const classes = useStyles(); + const knobs = [ ...Array(Math.floor((max - min) / step) + 1) ]; + + return ( +
+
    + {knobs.map((_, i) => ( +
  • ))} +
+
+ +
+ ); +} + +export default Slider; diff --git a/react/features/video-quality/components/VideoQualityDialog.web.js b/react/features/video-quality/components/VideoQualityDialog.web.js index 1fb7ef161..47b63112a 100644 --- a/react/features/video-quality/components/VideoQualityDialog.web.js +++ b/react/features/video-quality/components/VideoQualityDialog.web.js @@ -21,8 +21,8 @@ export default class VideoQualityDialog extends Component { return ( diff --git a/react/features/video-quality/components/VideoQualitySlider.web.js b/react/features/video-quality/components/VideoQualitySlider.web.js index 418914578..465e73b1e 100644 --- a/react/features/video-quality/components/VideoQualitySlider.web.js +++ b/react/features/video-quality/components/VideoQualitySlider.web.js @@ -1,5 +1,6 @@ // @flow - +import { withStyles } from '@material-ui/core/styles'; +import clsx from 'clsx'; import React, { Component } from 'react'; import type { Dispatch } from 'redux'; @@ -8,10 +9,13 @@ import { setAudioOnly } from '../../base/audio-only'; import { translate } from '../../base/i18n'; import { setLastN, getLastNForQualityLevel } from '../../base/lastn'; import { connect } from '../../base/redux'; +import { withPixelLineHeight } from '../../base/styles/functions.web'; import { setPreferredVideoQuality } from '../actions'; import { DEFAULT_LAST_N, VIDEO_QUALITY_LEVELS } from '../constants'; import logger from '../logger'; +import Slider from './Slider'; + const { ULTRA, HIGH, @@ -61,6 +65,11 @@ type Props = { */ _sendrecvVideoQuality: Number, + /** + * An object containing the CSS classes. + */ + classes: Object, + /** * Invoked to request toggling of audio only mode. */ @@ -72,6 +81,38 @@ type Props = { t: Function }; + +/** + * Creates the styles for the component. + * + * @param {Object} theme - The current UI theme. + * + * @returns {Object} + */ +const styles = theme => { + return { + dialog: { + color: theme.palette.text01 + }, + dialogDetails: { + ...withPixelLineHeight(theme.typography.bodyShortRegularLarge), + marginBottom: 16 + }, + dialogContents: { + background: theme.palette.ui01, + padding: '16px 16px 48px 16px' + }, + sliderDescription: { + ...withPixelLineHeight(theme.typography.heading6), + + display: 'flex', + justifyContent: 'space-between', + marginBottom: 40 + } + }; +}; + + /** * Implements a React {@link Component} which displays a slider for selecting a * new receive video quality. @@ -139,76 +180,29 @@ class VideoQualitySlider extends Component { * @returns {ReactElement} */ render() { - const { t } = this.props; + const { classes, t } = this.props; const activeSliderOption = this._mapCurrentQualityToSliderValue(); return ( -
-

- { t('videoStatus.callQuality') } -

-
-
- { /* FIXME: onChange and onMouseUp are both used for - * compatibility with IE11. This workaround can be - * removed after upgrading to React 16. - */ } - - -
-
- { this._createLabels(activeSliderOption) } +
+
{t('videoStatus.adjustFor')}
+
+
+ {t('videoStatus.bestPerformance')} + {t('videoStatus.highestQuality')}
+
); } - /** - * Creates React Elements to display mock tick marks with associated labels. - * - * @param {number} activeLabelIndex - Which of the sliderOptions should - * display as currently active. - * @private - * @returns {ReactElement[]} - */ - _createLabels(activeLabelIndex) { - const labelsCount = this._sliderOptions.length; - const maxWidthOfLabel = `${100 / labelsCount}%`; - - return this._sliderOptions.map((sliderOption, index) => { - const style = { - maxWidth: maxWidthOfLabel, - left: `${(index * 100) / (labelsCount - 1)}%` - }; - - const isActiveClass = activeLabelIndex === index ? 'active' : ''; - const className - = `video-quality-dialog-label-container ${isActiveClass}`; - - return ( -
-
- { this.props.t(sliderOption.textKey) } -
-
- ); - }); - } - _enableAudioOnly: () => void; /** @@ -386,4 +380,4 @@ function _mapStateToProps(state) { }; } -export default translate(connect(_mapStateToProps)(VideoQualitySlider)); +export default translate(connect(_mapStateToProps)(withStyles(styles)(VideoQualitySlider)));