feat(dialogs): Redesign Video Quality dialog & change dialog background color

This commit is contained in:
Vlad Piersec 2021-10-08 11:05:16 +03:00 committed by vp8x8
parent bad8911fe8
commit 1ad8f77179
16 changed files with 411 additions and 253 deletions

View File

@ -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.

View File

@ -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';

View File

@ -122,9 +122,6 @@
margin-bottom: 8px;
}
}
.modal-dialog-footer {
font-size: $modalButtonFontSize;
}
/**
* Styling inline dialog errors.

View File

@ -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;
}
}

View File

@ -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",

View File

@ -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 (
<JitsiThemeProvider>
<AtlasKitThemeProvider mode = 'dark'>
<GlobalStyles />
<ChromeExtensionBanner />
{ super._createMainElement(component, props) }
</AtlasKitThemeProvider>

View File

@ -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<Props> {
const {
id,
appearance,
classes,
heading,
hideCloseIconButton,
onClose,
@ -104,7 +142,9 @@ class ModalHeader extends React.Component<Props> {
}
return (
<Header showKeyline = { showKeyline }>
<Header
className = { classes.header }
showKeyline = { showKeyline }>
<Title>
<TitleIcon appearance = { appearance } />
<TitleText
@ -116,16 +156,21 @@ class ModalHeader extends React.Component<Props> {
</Title>
{
!hideCloseIconButton && <Icon
!hideCloseIconButton
&& <div
className = { classes.closeButton }
id = 'modal-header-close-button'
onClick = { onClose }>
<Icon
ariaLabel = { t('dialog.close') }
onClick = { onClose }
onKeyPress = { this._onKeyPress }
role = 'button'
src = { IconClose }
tabIndex = { 0 } />
</div>
}
</Header>
);
}
}
export default translate(ModalHeader);
export default translate(withStyles(styles)(ModalHeader));

View File

@ -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<Props> {
}
return (
<ModalFooter showKeyline = { propsFromModalFooter.showKeyline } >
<ModalFooter
className = { this.props.classes.footer }
showKeyline = { propsFromModalFooter.showKeyline } >
{
/**
@ -367,4 +388,4 @@ class StatelessDialog extends Component<Props> {
}
}
export default translate(StatelessDialog);
export default translate(withStyles(styles)(StatelessDialog));

View File

@ -9,5 +9,11 @@ export default createWebTheme({
colorMap,
spacing,
shape,
typography
typography,
breakpoints: {
values: {
'0': 0,
'480': 480
}
}
});

View File

@ -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;

View File

@ -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
}
}
};
};

View File

@ -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;
}

View File

@ -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;

View File

@ -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 (
<div className = { classes.sliderContainer }>
<ul className = { clsx(commonClassName.emptyList, classes.knobContainer) }>
{knobs.map((_, i) => (
<li
className = { classes.knob }
key = { `knob-${i}` } />))}
</ul>
<div className = { classes.track } />
<input
aria-label = { ariaLabel }
className = { clsx(classes.slider, 'custom-slider') }
max = { max }
min = { min }
onChange = { onChange }
step = { step }
type = 'range'
value = { value } />
</div>
);
}
export default Slider;

View File

@ -21,8 +21,8 @@ export default class VideoQualityDialog extends Component {
return (
<Dialog
hideCancelButton = { true }
okKey = 'dialog.done'
titleKey = 'videoStatus.callQuality'
submitDisabled = { true }
titleKey = 'videoStatus.performanceSettings'
width = 'small'>
<VideoQualitySlider />
</Dialog>

View File

@ -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,74 +180,27 @@ class VideoQualitySlider extends Component<Props> {
* @returns {ReactElement}
*/
render() {
const { t } = this.props;
const { classes, t } = this.props;
const activeSliderOption = this._mapCurrentQualityToSliderValue();
return (
<div className = { 'video-quality-dialog' }>
<h3 className = 'video-quality-dialog-title'>
{ t('videoStatus.callQuality') }
</h3>
<div className = 'video-quality-dialog-contents'>
<div className = 'video-quality-dialog-slider-container'>
{ /* FIXME: onChange and onMouseUp are both used for
* compatibility with IE11. This workaround can be
* removed after upgrading to React 16.
*/ }
<input
aria-label = { t('videoStatus.callQuality') }
className = 'video-quality-dialog-slider'
<div className = { clsx('video-quality-dialog', classes.dialog) }>
<div className = { classes.dialogDetails }>{t('videoStatus.adjustFor')}</div>
<div className = { classes.dialogContents }>
<div className = { classes.sliderDescription }>
<span>{t('videoStatus.bestPerformance')}</span>
<span>{t('videoStatus.highestQuality')}</span>
</div>
<Slider
ariaLabel = { t('videoStatus.callQuality') }
max = { this._sliderOptions.length - 1 }
min = '0'
min = { 0 }
onChange = { this._onSliderChange }
onMouseUp = { this._onSliderChange }
step = '1'
type = 'range'
value
= { activeSliderOption } />
</div>
<div className = 'video-quality-dialog-labels'>
{ this._createLabels(activeSliderOption) }
</div>
</div>
</div>
);
}
/**
* 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 (
<div
className = { className }
key = { index }
style = { style }>
<div className = 'video-quality-dialog-label'>
{ this.props.t(sliderOption.textKey) }
step = { 1 }
value = { activeSliderOption } />
</div>
</div>
);
});
}
_enableAudioOnly: () => void;
@ -386,4 +380,4 @@ function _mapStateToProps(state) {
};
}
export default translate(connect(_mapStateToProps)(VideoQualitySlider));
export default translate(connect(_mapStateToProps)(withStyles(styles)(VideoQualitySlider)));