feat(dialogs): Redesign Video Quality dialog & change dialog background color
This commit is contained in:
parent
bad8911fe8
commit
1ad8f77179
|
@ -24,61 +24,6 @@
|
||||||
bottom: calc(#{$newToolbarSizeWithPadding}) !important;
|
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
|
* Override @atlaskit/theme styling for the top toolbar so it displays over
|
||||||
* the video thumbnail while obscuring as little as possible.
|
* the video thumbnail while obscuring as little as possible.
|
||||||
|
|
|
@ -44,7 +44,6 @@ $flagsImagePath: "../images/";
|
||||||
@import 'modals/screen-share/share-audio';
|
@import 'modals/screen-share/share-audio';
|
||||||
@import 'modals/screen-share/share-screen-warning';
|
@import 'modals/screen-share/share-screen-warning';
|
||||||
@import 'modals/speaker_stats/speaker_stats';
|
@import 'modals/speaker_stats/speaker_stats';
|
||||||
@import 'modals/video-quality/video-quality';
|
|
||||||
@import 'modals/virtual-background/virtual-background';
|
@import 'modals/virtual-background/virtual-background';
|
||||||
@import 'modals/local-recording/local-recording';
|
@import 'modals/local-recording/local-recording';
|
||||||
@import 'videolayout_default';
|
@import 'videolayout_default';
|
||||||
|
|
|
@ -122,9 +122,6 @@
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.modal-dialog-footer {
|
|
||||||
font-size: $modalButtonFontSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Styling inline dialog errors.
|
* Styling inline dialog errors.
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1045,7 +1045,10 @@
|
||||||
"pending": "{{displayName}} has been invited"
|
"pending": "{{displayName}} has been invited"
|
||||||
},
|
},
|
||||||
"videoStatus": {
|
"videoStatus": {
|
||||||
|
"adjustFor": "Adjust for:",
|
||||||
"audioOnly": "AUD",
|
"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.",
|
"audioOnlyExpanded": "You are in low bandwidth mode. In this mode you will receive only audio and screen sharing.",
|
||||||
"callQuality": "Video Quality",
|
"callQuality": "Video Quality",
|
||||||
"hd": "HD",
|
"hd": "HD",
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { AtlasKitThemeProvider } from '@atlaskit/theme';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { DialogContainer } from '../../base/dialog';
|
import { DialogContainer } from '../../base/dialog';
|
||||||
|
import GlobalStyles from '../../base/ui/components/GlobalStyles';
|
||||||
import JitsiThemeProvider from '../../base/ui/components/JitsiThemeProvider';
|
import JitsiThemeProvider from '../../base/ui/components/JitsiThemeProvider';
|
||||||
import { ChromeExtensionBanner } from '../../chrome-extension-banner';
|
import { ChromeExtensionBanner } from '../../chrome-extension-banner';
|
||||||
|
|
||||||
|
@ -29,6 +30,7 @@ export class App extends AbstractApp {
|
||||||
return (
|
return (
|
||||||
<JitsiThemeProvider>
|
<JitsiThemeProvider>
|
||||||
<AtlasKitThemeProvider mode = 'dark'>
|
<AtlasKitThemeProvider mode = 'dark'>
|
||||||
|
<GlobalStyles />
|
||||||
<ChromeExtensionBanner />
|
<ChromeExtensionBanner />
|
||||||
{ super._createMainElement(component, props) }
|
{ super._createMainElement(component, props) }
|
||||||
</AtlasKitThemeProvider>
|
</AtlasKitThemeProvider>
|
||||||
|
|
|
@ -8,10 +8,12 @@ import {
|
||||||
titleIconWrapperStyles,
|
titleIconWrapperStyles,
|
||||||
TitleText
|
TitleText
|
||||||
} from '@atlaskit/modal-dialog/dist/es2019/styled/Content';
|
} from '@atlaskit/modal-dialog/dist/es2019/styled/Content';
|
||||||
|
import { withStyles } from '@material-ui/core/styles';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { translate } from '../../../i18n';
|
import { translate } from '../../../i18n';
|
||||||
import { Icon, IconClose } from '../../../icons';
|
import { Icon, IconClose } from '../../../icons';
|
||||||
|
import { withPixelLineHeight } from '../../../styles/functions';
|
||||||
|
|
||||||
const TitleIcon = ({ appearance }: { appearance?: 'danger' | 'warning' }) => {
|
const TitleIcon = ({ appearance }: { appearance?: 'danger' | 'warning' }) => {
|
||||||
if (!appearance) {
|
if (!appearance) {
|
||||||
|
@ -30,6 +32,7 @@ const TitleIcon = ({ appearance }: { appearance?: 'danger' | 'warning' }) => {
|
||||||
type Props = {
|
type Props = {
|
||||||
id: string,
|
id: string,
|
||||||
appearance?: 'danger' | 'warning',
|
appearance?: 'danger' | 'warning',
|
||||||
|
classes: Object,
|
||||||
heading: string,
|
heading: string,
|
||||||
hideCloseIconButton: boolean,
|
hideCloseIconButton: boolean,
|
||||||
onClose: Function,
|
onClose: Function,
|
||||||
|
@ -39,6 +42,40 @@ type Props = {
|
||||||
t: Function
|
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
|
* A default header for modal-dialog components
|
||||||
*
|
*
|
||||||
|
@ -90,6 +127,7 @@ class ModalHeader extends React.Component<Props> {
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
appearance,
|
appearance,
|
||||||
|
classes,
|
||||||
heading,
|
heading,
|
||||||
hideCloseIconButton,
|
hideCloseIconButton,
|
||||||
onClose,
|
onClose,
|
||||||
|
@ -104,7 +142,9 @@ class ModalHeader extends React.Component<Props> {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Header showKeyline = { showKeyline }>
|
<Header
|
||||||
|
className = { classes.header }
|
||||||
|
showKeyline = { showKeyline }>
|
||||||
<Title>
|
<Title>
|
||||||
<TitleIcon appearance = { appearance } />
|
<TitleIcon appearance = { appearance } />
|
||||||
<TitleText
|
<TitleText
|
||||||
|
@ -116,16 +156,21 @@ class ModalHeader extends React.Component<Props> {
|
||||||
</Title>
|
</Title>
|
||||||
|
|
||||||
{
|
{
|
||||||
!hideCloseIconButton && <Icon
|
!hideCloseIconButton
|
||||||
ariaLabel = { t('dialog.close') }
|
&& <div
|
||||||
onClick = { onClose }
|
className = { classes.closeButton }
|
||||||
onKeyPress = { this._onKeyPress }
|
id = 'modal-header-close-button'
|
||||||
role = 'button'
|
onClick = { onClose }>
|
||||||
src = { IconClose }
|
<Icon
|
||||||
tabIndex = { 0 } />
|
ariaLabel = { t('dialog.close') }
|
||||||
|
onKeyPress = { this._onKeyPress }
|
||||||
|
role = 'button'
|
||||||
|
src = { IconClose }
|
||||||
|
tabIndex = { 0 } />
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
</Header>
|
</Header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export default translate(ModalHeader);
|
export default translate(withStyles(styles)(ModalHeader));
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
import ButtonGroup from '@atlaskit/button/button-group';
|
import ButtonGroup from '@atlaskit/button/button-group';
|
||||||
import Button from '@atlaskit/button/standard-button';
|
import Button from '@atlaskit/button/standard-button';
|
||||||
import Modal, { ModalFooter } from '@atlaskit/modal-dialog';
|
import Modal, { ModalFooter } from '@atlaskit/modal-dialog';
|
||||||
|
import { withStyles } from '@material-ui/core/styles';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
@ -30,6 +31,11 @@ const OK_BUTTON_ID = 'modal-dialog-ok-button';
|
||||||
*/
|
*/
|
||||||
type Props = DialogProps & {
|
type Props = DialogProps & {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object containing the CSS classes.
|
||||||
|
*/
|
||||||
|
classes: Object,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom dialog header that replaces the standard heading.
|
* Custom dialog header that replaces the standard heading.
|
||||||
*/
|
*/
|
||||||
|
@ -101,6 +107,19 @@ type Props = DialogProps & {
|
||||||
width: string
|
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.
|
* Web dialog that uses atlaskit modal-dialog to display dialogs.
|
||||||
*/
|
*/
|
||||||
|
@ -205,7 +224,9 @@ class StatelessDialog extends Component<Props> {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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));
|
||||||
|
|
|
@ -9,5 +9,11 @@ export default createWebTheme({
|
||||||
colorMap,
|
colorMap,
|
||||||
spacing,
|
spacing,
|
||||||
shape,
|
shape,
|
||||||
typography
|
typography,
|
||||||
|
breakpoints: {
|
||||||
|
values: {
|
||||||
|
'0': 0,
|
||||||
|
'480': 480
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -13,8 +13,8 @@ const useStyles = makeStyles(() => {
|
||||||
return {
|
return {
|
||||||
speakerStatsSearch: {
|
speakerStatsSearch: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
right: '50px',
|
right: '80px',
|
||||||
top: '-5px'
|
top: '8px'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -70,4 +70,3 @@ function SpeakerStatsSearch({ onSearch }: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SpeakerStatsSearch;
|
export default SpeakerStatsSearch;
|
||||||
|
|
||||||
|
|
|
@ -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;
|
|
@ -21,8 +21,8 @@ export default class VideoQualityDialog extends Component {
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
hideCancelButton = { true }
|
hideCancelButton = { true }
|
||||||
okKey = 'dialog.done'
|
submitDisabled = { true }
|
||||||
titleKey = 'videoStatus.callQuality'
|
titleKey = 'videoStatus.performanceSettings'
|
||||||
width = 'small'>
|
width = 'small'>
|
||||||
<VideoQualitySlider />
|
<VideoQualitySlider />
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
import { withStyles } from '@material-ui/core/styles';
|
||||||
|
import clsx from 'clsx';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import type { Dispatch } from 'redux';
|
import type { Dispatch } from 'redux';
|
||||||
|
|
||||||
|
@ -8,10 +9,13 @@ import { setAudioOnly } from '../../base/audio-only';
|
||||||
import { translate } from '../../base/i18n';
|
import { translate } from '../../base/i18n';
|
||||||
import { setLastN, getLastNForQualityLevel } from '../../base/lastn';
|
import { setLastN, getLastNForQualityLevel } from '../../base/lastn';
|
||||||
import { connect } from '../../base/redux';
|
import { connect } from '../../base/redux';
|
||||||
|
import { withPixelLineHeight } from '../../base/styles/functions.web';
|
||||||
import { setPreferredVideoQuality } from '../actions';
|
import { setPreferredVideoQuality } from '../actions';
|
||||||
import { DEFAULT_LAST_N, VIDEO_QUALITY_LEVELS } from '../constants';
|
import { DEFAULT_LAST_N, VIDEO_QUALITY_LEVELS } from '../constants';
|
||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
|
|
||||||
|
import Slider from './Slider';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
ULTRA,
|
ULTRA,
|
||||||
HIGH,
|
HIGH,
|
||||||
|
@ -61,6 +65,11 @@ type Props = {
|
||||||
*/
|
*/
|
||||||
_sendrecvVideoQuality: Number,
|
_sendrecvVideoQuality: Number,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object containing the CSS classes.
|
||||||
|
*/
|
||||||
|
classes: Object,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invoked to request toggling of audio only mode.
|
* Invoked to request toggling of audio only mode.
|
||||||
*/
|
*/
|
||||||
|
@ -72,6 +81,38 @@ type Props = {
|
||||||
t: Function
|
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
|
* Implements a React {@link Component} which displays a slider for selecting a
|
||||||
* new receive video quality.
|
* new receive video quality.
|
||||||
|
@ -139,76 +180,29 @@ class VideoQualitySlider extends Component<Props> {
|
||||||
* @returns {ReactElement}
|
* @returns {ReactElement}
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
const { t } = this.props;
|
const { classes, t } = this.props;
|
||||||
const activeSliderOption = this._mapCurrentQualityToSliderValue();
|
const activeSliderOption = this._mapCurrentQualityToSliderValue();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className = { 'video-quality-dialog' }>
|
<div className = { clsx('video-quality-dialog', classes.dialog) }>
|
||||||
<h3 className = 'video-quality-dialog-title'>
|
<div className = { classes.dialogDetails }>{t('videoStatus.adjustFor')}</div>
|
||||||
{ t('videoStatus.callQuality') }
|
<div className = { classes.dialogContents }>
|
||||||
</h3>
|
<div className = { classes.sliderDescription }>
|
||||||
<div className = 'video-quality-dialog-contents'>
|
<span>{t('videoStatus.bestPerformance')}</span>
|
||||||
<div className = 'video-quality-dialog-slider-container'>
|
<span>{t('videoStatus.highestQuality')}</span>
|
||||||
{ /* 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'
|
|
||||||
max = { this._sliderOptions.length - 1 }
|
|
||||||
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>
|
||||||
|
<Slider
|
||||||
|
ariaLabel = { t('videoStatus.callQuality') }
|
||||||
|
max = { this._sliderOptions.length - 1 }
|
||||||
|
min = { 0 }
|
||||||
|
onChange = { this._onSliderChange }
|
||||||
|
step = { 1 }
|
||||||
|
value = { activeSliderOption } />
|
||||||
</div>
|
</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) }
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_enableAudioOnly: () => void;
|
_enableAudioOnly: () => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -386,4 +380,4 @@ function _mapStateToProps(state) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default translate(connect(_mapStateToProps)(VideoQualitySlider));
|
export default translate(connect(_mapStateToProps)(withStyles(styles)(VideoQualitySlider)));
|
||||||
|
|
Loading…
Reference in New Issue