feat(video-picker) Redesign (#12902)

Convert some files to TS
Implement redesign
Add Virtual background and Flip video to picker menu
pull/12909/head jitsi-meet_8310
Robert Pintilii 2 years ago committed by GitHub
parent 3cb0df579c
commit 27b8794d8c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      css/_audio-preview.scss
  2. 64
      css/_video-preview.scss
  3. 1
      lang/main.json
  4. 3
      react/features/base/media/components/Video.web.ts
  5. 15
      react/features/base/ui/components/web/ContextMenu.tsx
  6. 148
      react/features/settings/components/web/video/VideoSettingsContent.tsx
  7. 33
      react/features/settings/components/web/video/VideoSettingsPopup.tsx
  8. 2
      react/features/toolbox/components/web/Toolbox.tsx

@ -4,7 +4,7 @@
&-content {
position: relative;
right: auto;
margin-bottom: 8px;
margin-bottom: 4px;
max-height: 456px;
overflow: auto;
width: 300px;

@ -3,49 +3,38 @@
display: inline-block;
&-container {
max-height: 344px;
background: $menuBG;
border-radius: 3px;
max-height: 456px;
overflow: auto;
padding: 8px;
margin-bottom: 8px;
margin-bottom: 4px;
position: relative;
right: auto;
}
&-entry {
cursor: pointer;
height: 168px;
margin-bottom: 8px;
height: 138px;
width: 244px;
position: relative;
width: 284px;
margin: 0 7px 4px;
border-radius: 6px;
box-sizing: border-box;
overflow: hidden;
&:last-child {
margin-bottom: 0;
}
&--selected {
border: 3px solid #31B76A;
border-radius: 3px;
cursor: default;
height: 162px;
width: 278px;
border: 2px solid #4687ED;
}
}
&-video {
border-radius: 3px;
height: 100%;
object-fit: cover;
width: 100%;
}
&-overlay {
background: rgba(42, 58, 75, 0.6);
height: 100%;
position: absolute;
width: 100%;
z-index: 1;
}
&-error {
align-items: center;
display: flex;
@ -56,23 +45,22 @@
}
&-label {
bottom: 8px;
color: #fff;
position: absolute;
width: 100%;
bottom: 0;
left: 0;
right: 0;
max-width: 100%;
padding: 8px;
z-index: 2;
&-container {
margin: 0 16px;
}
&-text {
background-color: #131519;
border-radius: 3px;
padding: 2px 8px;
font-size: 13px;
line-height: 20px;
margin: 0 auto;
background-color: rgba(0, 0, 0, 0.7);
border-radius: 4px;
padding: 4px 8px;
color: #fff;
font-size: 12px;
line-height: 16px;
font-weight: 600;
max-width: calc(100% - 16px);
overflow: hidden;
text-overflow: ellipsis;
@ -80,8 +68,8 @@
white-space: nowrap;
}
}
// Override @atlaskit/InlineDialog container which is made with styled components
& > div:nth-child(2) {
padding: 0;
&-checkbox-container {
padding: 10px 14px;
}
}

@ -1286,6 +1286,7 @@
"grantModerator": "Grant Moderator Rights",
"hideSelfView": "Hide self view",
"kick": "Kick out",
"mirrorVideo": "Mirror my video",
"moderator": "Moderator",
"mute": "Participant is muted",
"muted": "Muted",

@ -1,5 +1,4 @@
// @flow
// @ts-ignore
import Video from './web/Video';
export default Video;

@ -34,6 +34,9 @@ const getComputedOuterHeight = (element: HTMLElement) => {
interface IProps {
/**
* ARIA attributes.
*/
[key: `aria-${string}`]: string;
/**
@ -106,6 +109,11 @@ interface IProps {
*/
onMouseLeave?: (e?: React.MouseEvent) => void;
/**
* Container role.
*/
role?: string;
/**
* Tab index for the menu.
*/
@ -167,7 +175,9 @@ const ContextMenu = ({
onDrawerClose,
onMouseEnter,
onMouseLeave,
tabIndex
role,
tabIndex,
...aria
}: IProps) => {
const [ isHidden, setIsHidden ] = useState(true);
const containerRef = useRef<HTMLDivElement | null>(null);
@ -225,6 +235,7 @@ const ContextMenu = ({
</Drawer>
</JitsiPortal>
: <div
{ ...aria }
aria-label = { accessibilityLabel }
className = { cx(participantsPaneTheme.ignoredChildClassName,
styles.contextMenu,
@ -237,7 +248,7 @@ const ContextMenu = ({
onMouseEnter = { onMouseEnter }
onMouseLeave = { onMouseLeave }
ref = { containerRef }
role = 'menu'
role = { role ?? 'menu' }
tabIndex = { tabIndex }>
{children}
</div>;

@ -1,45 +1,63 @@
// @flow
import React, { Component } from 'react';
import { translate } from '../../../../base/i18n';
import Video from '../../../../base/media/components/Video';
import { equals } from '../../../../base/redux';
import { createLocalVideoTracks } from '../../../functions';
import { WithTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { IReduxState, IStore } from '../../../../app/types';
import { openDialog } from '../../../../base/dialog/actions';
import { translate } from '../../../../base/i18n/functions';
import { IconImage } from '../../../../base/icons/svg';
import Video from '../../../../base/media/components/Video.web';
import { equals } from '../../../../base/redux/functions';
import { updateSettings } from '../../../../base/settings/actions';
import Checkbox from '../../../../base/ui/components/web/Checkbox';
import ContextMenu from '../../../../base/ui/components/web/ContextMenu';
import ContextMenuItem from '../../../../base/ui/components/web/ContextMenuItem';
import ContextMenuItemGroup from '../../../../base/ui/components/web/ContextMenuItemGroup';
import VirtualBackgroundDialog from '../../../../virtual-background/components/VirtualBackgroundDialog';
import { createLocalVideoTracks } from '../../../functions.web';
const videoClassName = 'video-preview-video flipVideoX';
/**
* The type of the React {@code Component} props of {@link VideoSettingsContent}.
*/
export type Props = {
export interface IProps extends WithTranslation {
/**
* Callback to change the flip state.
*/
changeFlip: (flip: boolean) => void;
/**
* The deviceId of the camera device currently being used.
*/
currentCameraDeviceId: string,
currentCameraDeviceId: string;
/**
* Callback invoked to change current camera.
* Whether or not the local video is flipped.
*/
setVideoInputDevice: Function,
localFlipX: boolean;
/**
* Invoked to obtain translated strings.
* Open virtual background dialog.
*/
selectBackground: () => void;
/**
* Callback invoked to change current camera.
*/
t: Function,
setVideoInputDevice: Function;
/**
* Callback invoked to toggle the settings popup visibility.
*/
toggleVideoSettings: Function,
toggleVideoSettings: Function;
/**
* All the camera device ids currently connected.
*/
videoDeviceIds: string[],
};
videoDeviceIds: string[];
}
/**
* The type of the React {@code Component} state of {@link VideoSettingsContent}.
@ -49,7 +67,7 @@ type State = {
/**
* An array of all the jitsiTracks and eventual errors.
*/
trackData: Object[],
trackData: { deviceId: string; error?: string; jitsiTrack: any | null; }[];
};
/**
@ -58,9 +76,8 @@ type State = {
*
* @augments Component
*/
class VideoSettingsContent extends Component<Props, State> {
class VideoSettingsContent extends Component<IProps, State> {
_componentWasUnmounted: boolean;
_videoContentRef: Object;
/**
* Initializes a new {@code VideoSettingsContent} instance.
@ -68,10 +85,9 @@ class VideoSettingsContent extends Component<Props, State> {
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props) {
constructor(props: IProps) {
super(props);
this._onEscClick = this._onEscClick.bind(this);
this._videoContentRef = React.createRef();
this._onToggleFlip = this._onToggleFlip.bind(this);
this.state = {
trackData: new Array(props.videoDeviceIds.length).fill({
@ -79,20 +95,16 @@ class VideoSettingsContent extends Component<Props, State> {
})
};
}
_onEscClick: (KeyboardEvent) => void;
/**
* Click handler for the video entries.
* Toggles local video flip state.
*
* @param {KeyboardEvent} event - Esc key click to close the popup.
* @returns {void}
*/
_onEscClick(event) {
if (event.key === 'Escape') {
event.preventDefault();
event.stopPropagation();
this._videoContentRef.current.style.display = 'none';
}
_onToggleFlip() {
const { localFlipX, changeFlip } = this.props;
changeFlip(!localFlipX);
}
/**
@ -122,9 +134,9 @@ class VideoSettingsContent extends Component<Props, State> {
* @param {Object[]} trackData - An array of tracks that are to be disposed.
* @returns {Promise<void>}
*/
_disposeTracks(trackData) {
_disposeTracks(trackData: { jitsiTrack: any; }[]) {
trackData.forEach(({ jitsiTrack }) => {
jitsiTrack && jitsiTrack.dispose();
jitsiTrack?.dispose();
});
}
@ -134,7 +146,7 @@ class VideoSettingsContent extends Component<Props, State> {
* @param {string} deviceId - The id of the camera device.
* @returns {Function}
*/
_onEntryClick(deviceId) {
_onEntryClick(deviceId: string) {
return () => {
this.props.setVideoInputDevice(deviceId);
this.props.toggleVideoSettings();
@ -148,7 +160,7 @@ class VideoSettingsContent extends Component<Props, State> {
* @param {number} index - The index of the entry.
* @returns {React$Node}
*/
_renderPreviewEntry(data, index) {
_renderPreviewEntry(data: { deviceId: string; error?: string; jitsiTrack: any | null; }, index: number) {
const { error, jitsiTrack, deviceId } = data;
const { currentCameraDeviceId, t } = this.props;
const isSelected = deviceId === currentCameraDeviceId;
@ -167,19 +179,19 @@ class VideoSettingsContent extends Component<Props, State> {
);
}
const props: Object = {
const props: any = {
className,
key,
tabIndex
};
const label = jitsiTrack && jitsiTrack.getTrackLabel();
const label = jitsiTrack?.getTrackLabel();
if (isSelected) {
props['aria-checked'] = true;
props.className = `${className} video-preview-entry--selected`;
} else {
props.onClick = this._onEntryClick(deviceId);
props.onKeyPress = e => {
props.onKeyPress = (e: React.KeyboardEvent) => {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
props.onClick();
@ -192,12 +204,10 @@ class VideoSettingsContent extends Component<Props, State> {
{ ...props }
role = 'radio'>
<div className = 'video-preview-label'>
{label && <div className = 'video-preview-label-container'>
<div className = 'video-preview-label-text'>
<span>{label}</span></div>
{label && <div className = 'video-preview-label-text'>
<span>{label}</span>
</div>}
</div>
<div className = 'video-preview-overlay' />
<Video
className = { videoClassName }
playsinline = { true }
@ -230,7 +240,7 @@ class VideoSettingsContent extends Component<Props, State> {
*
* @inheritdoc
*/
componentDidUpdate(prevProps) {
componentDidUpdate(prevProps: IProps) {
if (!equals(this.props.videoDeviceIds, prevProps.videoDeviceIds)) {
this._setTracks();
}
@ -243,21 +253,57 @@ class VideoSettingsContent extends Component<Props, State> {
*/
render() {
const { trackData } = this.state;
const { selectBackground, t, localFlipX } = this.props;
return (
<div
<ContextMenu
aria-labelledby = 'video-settings-button'
className = 'video-preview-container'
hidden = { false }
id = 'video-settings-dialog'
onKeyDown = { this._onEscClick }
ref = { this._videoContentRef }
role = 'radiogroup'
tabIndex = '-1'>
{trackData.map((data, i) => this._renderPreviewEntry(data, i))}
</div>
tabIndex = { -1 }>
<ContextMenuItemGroup>
{trackData.map((data, i) => this._renderPreviewEntry(data, i))}
</ContextMenuItemGroup>
<ContextMenuItemGroup>
<ContextMenuItem
accessibilityLabel = 'virtualBackground.title'
icon = { IconImage }
onClick = { selectBackground }
text = { t('virtualBackground.title') } />
<div
className = 'video-preview-checkbox-container'
// eslint-disable-next-line react/jsx-no-bind
onClick = { e => e.stopPropagation() }>
<Checkbox
checked = { localFlipX }
label = { t('videothumbnail.mirrorVideo') }
onChange = { this._onToggleFlip } />
</div>
</ContextMenuItemGroup>
</ContextMenu>
);
}
}
const mapStateToProps = (state: IReduxState) => {
const { localFlipX } = state['features/base/settings'];
return {
localFlipX: Boolean(localFlipX)
};
};
const mapDispatchToProps = (dispatch: IStore['dispatch']) => {
return {
selectBackground: () => dispatch(openDialog(VirtualBackgroundDialog)),
changeFlip: (flip: boolean) => {
dispatch(updateSettings({
localFlipX: flip
}));
}
};
};
export default translate(VideoSettingsContent);
export default translate(connect(mapStateToProps, mapDispatchToProps)(VideoSettingsContent));

@ -1,7 +1,7 @@
// @flow
import React from 'react';
import React, { ReactNode } from 'react';
import { connect } from 'react-redux';
import { IReduxState } from '../../../../app/types';
import {
setVideoInputDeviceAndUpdateSettings
} from '../../../../base/devices/actions.web';
@ -9,36 +9,35 @@ import {
getVideoDeviceIds
} from '../../../../base/devices/functions.web';
import Popover from '../../../../base/popover/components/Popover.web';
import { connect } from '../../../../base/redux';
import { SMALL_MOBILE_WIDTH } from '../../../../base/responsive-ui/constants';
import { getCurrentCameraDeviceId } from '../../../../base/settings';
import { getCurrentCameraDeviceId } from '../../../../base/settings/functions.web';
import { toggleVideoSettings } from '../../../actions';
import { getVideoSettingsVisibility } from '../../../functions';
import { getVideoSettingsVisibility } from '../../../functions.web';
import VideoSettingsContent, { type Props as VideoSettingsProps } from './VideoSettingsContent';
import VideoSettingsContent, { type IProps as VideoSettingsProps } from './VideoSettingsContent';
type Props = VideoSettingsProps & {
interface IProps extends VideoSettingsProps {
/**
* Component children (the Video button).
*/
children: React$Node,
children: ReactNode;
/**
* Flag controlling the visibility of the popup.
*/
isOpen: boolean,
isOpen: boolean;
/**
* Callback executed when the popup closes.
*/
onClose: Function,
onClose: Function;
/**
* The popup placement enum value.
*/
popupPlacement: string
popupPlacement: string;
}
/**
@ -54,7 +53,7 @@ function VideoSettingsPopup({
popupPlacement,
setVideoInputDevice,
videoDeviceIds
}: Props) {
}: IProps) {
return (
<div className = 'video-preview'>
<Popover
@ -80,14 +79,14 @@ function VideoSettingsPopup({
* @param {Object} state - Redux state.
* @returns {Object}
*/
function mapStateToProps(state) {
function mapStateToProps(state: IReduxState) {
const { clientWidth } = state['features/base/responsive-ui'];
return {
currentCameraDeviceId: getCurrentCameraDeviceId(state),
isOpen: getVideoSettingsVisibility(state),
popupPlacement: clientWidth <= SMALL_MOBILE_WIDTH ? 'auto' : 'top-end',
videoDeviceIds: getVideoDeviceIds(state)
isOpen: Boolean(getVideoSettingsVisibility(state)),
popupPlacement: clientWidth <= Number(SMALL_MOBILE_WIDTH) ? 'auto' : 'top-end',
videoDeviceIds: getVideoDeviceIds(state) ?? []
};
}

@ -377,7 +377,7 @@ const styles = () => {
rowGap: '8px',
margin: 0,
padding: '16px',
marginBottom: '8px'
marginBottom: '4px'
}
};
};

Loading…
Cancel
Save